Skip to content

Commit

Permalink
fix(oauth-provider): better align errors with spec
Browse files Browse the repository at this point in the history
  • Loading branch information
matthieusieben committed Apr 24, 2024
1 parent 7082971 commit da4e905
Show file tree
Hide file tree
Showing 24 changed files with 260 additions and 141 deletions.
6 changes: 3 additions & 3 deletions packages/oauth-provider/src/account/account-manager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { OAuthClientId } from '@atproto/oauth-client-metadata'
import { DeviceId } from '../device/device-id.js'
import { UnauthorizedError } from '../errors/unauthorized-error.js'
import { InvalidRequestError } from '../oauth-errors.js'
import { Sub } from '../oidc/sub.js'
import { constantTime } from '../util/time.js'
import { AccountInfo, AccountStore, LoginCredentials } from './account-store.js'
Expand All @@ -18,15 +18,15 @@ export class AccountManager {
const result = await this.store.authenticateAccount(credentials, deviceId)
if (result) return result

throw new UnauthorizedError('Invalid credentials', {})
throw new InvalidRequestError('Invalid credentials')
})
}

public async get(deviceId: DeviceId, sub: Sub): Promise<AccountInfo> {
const result = await this.store.getDeviceAccount(deviceId, sub)
if (result) return result

throw new UnauthorizedError(`Account not found`, {})
throw new InvalidRequestError(`Account not found`)
}

public async addAuthorizedClient(
Expand Down
11 changes: 6 additions & 5 deletions packages/oauth-provider/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { JOSEError } from 'jose/errors'

import { CLIENT_ASSERTION_MAX_AGE, JAR_MAX_AGE } from '../constants.js'
import { InvalidClientError } from '../errors/invalid-client-error.js'
import { InvalidClientMetadataError } from '../errors/invalid-client-metadata-error.js'
import { InvalidRequestError } from '../errors/invalid-request-error.js'
import { ClientAuth, authJwkThumbprint } from './client-auth.js'
Expand Down Expand Up @@ -145,19 +146,19 @@ export class Client {
maxTokenAge: CLIENT_ASSERTION_MAX_AGE / 1000,
}).catch((err) => {
if (err instanceof JOSEError) {
const msg = `Invalid "client_assertion": ${err.message}`
throw new InvalidRequestError(msg, err)
const msg = `Validation of "client_assertion" failed: ${err.message}`
throw new InvalidClientError(msg, err)
}

throw err
})

if (!result.protectedHeader.kid) {
throw new InvalidRequestError(`"kid" required in client_assertion`)
throw new InvalidClientError(`"kid" required in client_assertion`)
}

if (!result.payload.jti) {
throw new InvalidRequestError(`"jti" required in client_assertion`)
throw new InvalidClientError(`"jti" required in client_assertion`)
}

const clientAuth: ClientAuth = {
Expand All @@ -170,7 +171,7 @@ export class Client {
return { clientAuth, nonce: result.payload.jti }
}

throw new InvalidRequestError(
throw new InvalidClientError(
`Unsupported client_assertion_type "${input.client_assertion_type}"`,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { OAuthError } from './oauth-error.js'

/**
* @see {@link https://datatracker.ietf.org/doc/html/rfc9396#section-14.6 | RFC 9396, Section 14.6}
* @see
* {@link https://datatracker.ietf.org/doc/html/rfc9396#section-14.6 | RFC 9396 - OAuth Dynamic Client Registration Metadata Registration Error}
*
* The AS MUST refuse to process any unknown authorization details type or
* authorization details not conforming to the respective type definition. The
* AS MUST abort processing and respond with an error
* invalid_authorization_details to the client if any of the following are true
* of the objects in the authorization_details structure:
* - contains an unknown authorization details type value,
* - is an object of known type but containing unknown fields,
* - contains fields of the wrong type for the authorization details type,
* - contains fields with invalid values for the authorization details type, or
* - is missing required fields for the authorization details type.
*/
export class InvalidAuthorizationDetailsError extends OAuthError {
constructor(error_description: string, cause?: unknown) {
Expand Down
18 changes: 14 additions & 4 deletions packages/oauth-provider/src/errors/invalid-client-error.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { OAuthError } from './oauth-error.js'

/**
* @see {@link https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.2 | RFC7591}
* @see
* {@link https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 | RFC6749 - Issuing an Access Token }
*
* Client authentication failed (e.g., unknown client, no client authentication
* included, or unsupported authentication method). The authorization server MAY
* return an HTTP 401 (Unauthorized) status code to indicate which HTTP
* authentication schemes are supported. If the client attempted to
* authenticate via the "Authorization" request header field, the authorization
* server MUST respond with an HTTP 401 (Unauthorized) status code and include
* the "WWW-Authenticate" response header field matching the authentication
* scheme used by the client.
*/
export abstract class InvalidClientError extends OAuthError {
constructor(error: string, error_description: string, cause?: unknown) {
super(error, error_description, 400, cause)
export class InvalidClientError extends OAuthError {
constructor(error_description: string, cause?: unknown) {
super('invalid_client', error_description, 400, cause)
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { InvalidClientError } from './invalid-client-error.js'
import { OAuthError } from './oauth-error.js'

/**
* @see {@link https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.2 | RFC7591}
* @see {@link https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.2 | RFC7591 - Client Registration Error Response}
*
* The value of one of the client metadata fields is invalid and the server has
* rejected this request. Note that an authorization server MAY choose to
* substitute a valid value for any requested parameter of a client's metadata.
*/
export class InvalidClientMetadataError extends InvalidClientError {
export class InvalidClientMetadataError extends OAuthError {
constructor(error_description: string, cause?: unknown) {
super('invalid_client_metadata', error_description, cause)
super('invalid_client_metadata', error_description, 400, cause)
}
}
20 changes: 17 additions & 3 deletions packages/oauth-provider/src/errors/invalid-dpop-key-binding.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
import { InvalidTokenError } from './invalid-token-error.js'
import { WWWAuthenticateError } from './www-authenticate-error.js'

export class InvalidDpopKeyBindingError extends InvalidTokenError {
/**
* @see
* {@link https://datatracker.ietf.org/doc/html/rfc6750#section-3.1 | RFC6750 - The WWW-Authenticate Response Header Field}
*
* @see
* {@link https://datatracker.ietf.org/doc/html/rfc9449#name-the-dpop-authentication-sch | RFC9449 - The DPoP Authentication Scheme}
*/
export class InvalidDpopKeyBindingError extends WWWAuthenticateError {
constructor(cause?: unknown) {
super('Invalid DPoP key binding', { DPoP: {} }, cause)
const error = 'invalid_token'
const error_description = 'Invalid DPoP key binding'
super(
error,
error_description,
{ DPoP: { error, error_description } },
cause,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export class InvalidDpopProofError extends WWWAuthenticateError {
super(
error,
error_description,
400,
{ DPoP: { error, error_description } },
cause,
)
Expand Down
16 changes: 16 additions & 0 deletions packages/oauth-provider/src/errors/invalid-grant-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { OAuthError } from './oauth-error.js'

/**
* @see
* {@link https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 | RFC6749 - Issuing an Access Token }
*
* The provided authorization grant (e.g., authorization code, resource owner
* credentials) or refresh token is invalid, expired, revoked, does not match
* the redirection URI used in the authorization request, or was issued to
* another client.
*/
export class InvalidGrantError extends OAuthError {
constructor(error_description: string, cause?: unknown) {
super('invalid_grant', error_description, 400, cause)
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { InvalidClientError } from './invalid-client-error.js'
import { OAuthError } from './oauth-error.js'

/**
* @see {@link https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.2 | RFC7591}
*
* The value of one or more redirection URIs is invalid.
*/
export class InvalidRedirectUriError extends InvalidClientError {
export class InvalidRedirectUriError extends OAuthError {
constructor(error_description: string, cause?: unknown) {
super('invalid_redirect_uri', error_description, cause)
super('invalid_redirect_uri', error_description, 400, cause)
}
}
23 changes: 23 additions & 0 deletions packages/oauth-provider/src/errors/invalid-request-error.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
import { OAuthError } from './oauth-error.js'

/**
* @see
* {@link https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 | RFC6749 - Issuing an Access Token }
*
* The request is missing a required parameter, includes an unsupported
* parameter value (other than grant type), repeats a parameter, includes
* multiple credentials, utilizes more than one mechanism for authenticating the
* client, or is otherwise malformed.
*
* @see
* {@link https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1 | RFC6749 - Authorization Code Grant, Authorization Request}
*
* The request is missing a required parameter, includes an invalid parameter
* value, includes a parameter more than once, or is otherwise malformed.
*
* @see
* {@link https://datatracker.ietf.org/doc/html/rfc6750#section-3.1 | RFC6750 - The WWW-Authenticate Response Header Field }
*
* The request is missing a required parameter, includes an unsupported
* parameter or parameter value, repeats the same parameter, uses more than one
* method for including an access token, or is otherwise malformed. The resource
* server SHOULD respond with the HTTP 400 (Bad Request) status code.
*/
export class InvalidRequestError extends OAuthError {
constructor(error_description: string, cause?: unknown) {
super('invalid_request', error_description, 400, cause)
Expand Down
46 changes: 37 additions & 9 deletions packages/oauth-provider/src/errors/invalid-token-error.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,58 @@
import { JOSEError } from 'jose/errors'
import { ZodError } from 'zod'

import { UnauthorizedError, WWWAuthenticate } from './unauthorized-error.js'
import { OAuthError } from './oauth-error.js'
import { WWWAuthenticateError } from './www-authenticate-error.js'

export class InvalidTokenError extends UnauthorizedError {
/**
* @see
* {@link https://datatracker.ietf.org/doc/html/rfc6750#section-3.1 | RFC6750 - The WWW-Authenticate Response Header Field }
*
* The access token provided is expired, revoked, malformed, or invalid for
* other reasons. The resource SHOULD respond with the HTTP 401 (Unauthorized)
* status code. The client MAY request a new access token and retry the
* protected resource request.
*/
export class InvalidTokenError extends WWWAuthenticateError {
static from(
err: unknown,
wwwAuthenticate: WWWAuthenticate,
fallbackMessage = 'Invalid token',
tokenType: string,
fallbackMessage?: string,
): InvalidTokenError {
if (err instanceof InvalidTokenError) {
return err
}

if (err instanceof OAuthError) {
return new InvalidTokenError(tokenType, err.error_description, err)
}

if (err instanceof JOSEError) {
throw new InvalidTokenError(err.message, wwwAuthenticate, err)
return new InvalidTokenError(tokenType, err.message, err)
}

if (err instanceof ZodError) {
throw new InvalidTokenError(err.message, wwwAuthenticate, err)
return new InvalidTokenError(tokenType, err.message, err)
}

throw new InvalidTokenError(fallbackMessage, wwwAuthenticate, err)
return new InvalidTokenError(
tokenType,
fallbackMessage ?? 'Invalid token',
err,
)
}

constructor(
readonly tokenType: string,
error_description: string,
wwwAuthenticate: WWWAuthenticate,
cause?: unknown,
) {
super(error_description, wwwAuthenticate, cause)
const error = 'invalid_token'
super(
error,
error_description,
{ [tokenType]: { error, error_description } },
cause,
)
}
}
15 changes: 14 additions & 1 deletion packages/oauth-provider/src/errors/unauthorized-client-error.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import { OAuthError } from './oauth-error.js'

/**
* @see
* {@link https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 | RFC6749 - Issuing an Access Token }
*
* The authenticated client is not authorized to use this authorization grant
* type.
*
* @see
* {@link https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1 | RFC6749 - Authorization Code Grant, Authorization Request}
*
* The client is not authorized to request an authorization code using this
* method.
*/
export class UnauthorizedClientError extends OAuthError {
constructor(error_description: string, cause?: unknown) {
super('unauthorized_client', error_description, 401, cause)
super('unauthorized_client', error_description, 400, cause)
}
}
7 changes: 0 additions & 7 deletions packages/oauth-provider/src/errors/unauthorized-dpop-error.ts

This file was deleted.

21 changes: 0 additions & 21 deletions packages/oauth-provider/src/errors/unauthorized-error.ts

This file was deleted.

7 changes: 0 additions & 7 deletions packages/oauth-provider/src/errors/unknown-request-error.ts

This file was deleted.

21 changes: 20 additions & 1 deletion packages/oauth-provider/src/errors/use-dpop-nonce-error.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { OAuthError } from './oauth-error.js'
import { WWWAuthenticateError } from './www-authenticate-error.js'

/**
* @see {@link https://datatracker.ietf.org/doc/html/rfc9449#section-8}
* @see
* {@link https://datatracker.ietf.org/doc/html/rfc9449#section-8 | RFC9449 - Authorization Server-Provided Nonce}
*/
export class UseDpopNonceError extends OAuthError {
constructor(
Expand All @@ -10,4 +12,21 @@ export class UseDpopNonceError extends OAuthError {
) {
super('use_dpop_nonce', error_description, 400, cause)
}

/**
* Convert this error into an error meant to be used as "Resource
* Server-Provided Nonce" error.
*
* @see
* {@link https://datatracker.ietf.org/doc/html/rfc9449#name-resource-server-provided-no | RFC9449 - Resource Server-Provided Nonce}
*/
toWwwAuthenticateError(): WWWAuthenticateError {
const { error, error_description } = this
throw new WWWAuthenticateError(
error,
error_description,
{ DPoP: { error, error_description } },
this,
)
}
}

0 comments on commit da4e905

Please sign in to comment.