Skip to content

OAuthenticator should manage DPoP signing #43

@ThisIsMissEm

Description

@ThisIsMissEm

Signing with DPoP is required for AT Protocol:

DPoP is mandatory for all clients, so this must be present and true
https://atproto.com/specs/oauth#demonstrating-proof-of-possession-d-po-p

The current design of OAuthenticator makes it the consumer's responsibility to correctly manage DPoP which is a pretty complicated task. For example, DPoP nonces could be different depending on which server you're talking to, e.g., if the resource server and authorization server are different, then you may have multiple DPoP Nonces, one for each. Figuring out what parameters the DPoP JWT requires is also somewhat non-trivial.

It would require bringing in more dependencies, but would simplify the interface for consumers. (e.g., JWT generation dependencies)

The Login struct should also persist the DPoP Private Key, and we'd likely want to provide a simple key/value storage for storing DPoP Nonces ( host -> nonce ), such that when making an authenticated request we can retrieve the correct nonce for the request host.

So we would add:

  • add dpopKey to Login, which can be nil
  • a DPoPStorage interface & input to Configuration which is a simple key/value store, where the key is the host, and the value is the nonce for that host.

Currently the logic for defining the DPoP JWTs looks like the following:

		public static func makeDpopSigner(p256Key: P256.Signing.PrivateKey) throws
			-> DPoPSigner.JWTGenerator
		{
			{ (parameters: DPoPSigner.JWTParameters) async throws -> String in
				let payload: any Encodable = {
					if let nonce = parameters.nonce,
						let authorizationServerIssuer = parameters
							.issuingServer,
						let accessTokenHash = parameters.tokenHash
					{
						DPoPRequestPayload(
							httpMethod: parameters.httpMethod,
							httpRequestURL: parameters.requestEndpoint,
							createdAt: Int(
								Date.now.timeIntervalSince1970),
							expiresAt: Int(
								Date.now.timeIntervalSince1970
									+ 3600),
							nonce: nonce,
							authorizationServerIssuer:
								authorizationServerIssuer,
							accessTokenHash: accessTokenHash
						)
					} else {
						DPoPTokenPayload(
							httpMethod: parameters.httpMethod,
							httpRequestURL: parameters.requestEndpoint,
							createdAt: Int(
								Date.now.timeIntervalSince1970),
							expiresAt: Int(
								Date.now.timeIntervalSince1970
									+ 3600),
							nonce: parameters.nonce
						)
					}
				}()

				return try await JWTSerializerLite.sign(
					payload,
					with: JWTLexiconLite.JWTHeader(
						typ: parameters.keyType,
						jwk: JWTLexiconLite.JWK(key: p256Key)
					),
					using: ECDSASigner(key: p256Key)
				)
			}
		}
	}

However, this is always going to be the same for all AT Protocol client implementations. Therefore it should probably be internal to the AT Protocol implementation in OAuthenticator

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions