Example of a candidate approach for API keys. This approach:
-
Uses ssh keys (via the ssh-agent) on clients to provide identity. The ssh-agent is tasked with ssh key operations - this code never touches the client's private ssh key.
-
Does not centralize any private client key. Users can make their own ssh keys and submit the public key only for identification / usage.
-
Issues a signed JWT token with issue and expirataion times, and as the ip network that the key is valid for.
-
end-users can have a shorter validity and a single address network.
-
internal users can have a longer validty and the internal subnet.
-
-
Implementation uses standard python SSH / JOSE / JWT / JWK constructs and packages.
-
Verification requires only the issuer public key and a token provided (and signed) by the issuer. The issuer is not required to be available in order to verify an issued token.
-
Auth exchange is based on OAUTH, with client_id and PCKE, but with only two counterparties - the client and the issuer.
- SSH Agent running on the client, with the required key added.
- Issuer routes availabe to the client
tc = TokenClient(logger=logger)
if tc.use('SHA256:<HASH>'):
token = tc.access_token
-
Client GETs a nonce from the issuer, also providing the client_id (the public ssh key hash), and also a state + PCKE challenge as part of the request.
-
Issuer responses with a nonce (16 bytes of random data) and the state provided by the client, all signed with the Issuer's private key.
-
Client verifies the signed response from the Issue via the Issuer's public key.
-
Client uses ssh agent to sign the nonce.
-
Client POSTS the signed nonce data, the state + PCKE verifier.
-
Issuer verifies the PCKE verifier and the signature on the signed data.
-
Issuer generates claim for the client (a dictionary) with timestamps and network, signs this with the Issuer's private key, and issues the result as a token to the client.
-
Client uses the provided token in the
Authorization
http header.
if token.valid:
headers={'Authorization': f"Bearer {token!s}"
if TokenVerifier.verify_token(token, client_ip, issuer_public_key):
- Provides four routes:
-
GET
/nonce
initial nonce generation and private storage of the nonce, PCKE challenge, and client_id
-
POST
/nonce
verification of client's data (signature, PCKE, etc). token issue
-
GET
/jwks
covenience function to provide Issuers public key, useful if it isn't stored on the client (or server).
-
GET
/verify
convenience function for a client to check if their token is valid. a simple wrapper around
TokenVerifier.verify_token
where the client provides the token via theAuthorization
header.
-
Issuer private key can be generated at run-time (means all tokens are invalidated asof when a Server read the updated public key via the
/jwks
route). Or stored in an Issuer config. -
Client public key fingerprints can be stored in a configuration.
-
Issuer public key can be requested from the Issuer or stored in a configuration.
-
Issuer can produce a token outside of the client verification with:
network_access_token = await issuer.generate_access_token(datetime.timedelta(days=7), ipaddress.ip_network("10.0.0.0/16"))