Skip to content

Commit

Permalink
feat(oauth-provider): add onAccountAddAuthorizedClient hook
Browse files Browse the repository at this point in the history
  • Loading branch information
matthieusieben committed Apr 24, 2024
1 parent 1f32320 commit c4d53b7
Show file tree
Hide file tree
Showing 13 changed files with 266 additions and 185 deletions.
26 changes: 26 additions & 0 deletions packages/oauth-provider/src/account/account-hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Client } from '../client/client.js'
import { DeviceId } from '../device/device-id.js'
import { ClientAuth } from '../token/token-store.js'
import { Awaitable } from '../util/awaitable.js'
import { Account } from './account-store.js'
// https://github.com/typescript-eslint/typescript-eslint/issues/8902
// eslint-disable-next-line
import { AccountStore } from './account-store.js'

/**
* Allows disabling the call to {@link AccountStore.addAuthorizedClient} based
* on the account, client and clientAuth (not all these info are available to
* the store method).
*/
export type AccountAddAuthorizedClient = (
client: Client,
data: {
deviceId: DeviceId
account: Account
clientAuth: ClientAuth
},
) => Awaitable<boolean>

export type AccountHooks = {
onAccountAddAuthorizedClient?: AccountAddAuthorizedClient
}
32 changes: 26 additions & 6 deletions packages/oauth-provider/src/account/account-manager.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import { OAuthClientId } from '@atproto/oauth-client-metadata'
import { Client } from '../client/client.js'
import { DeviceId } from '../device/device-id.js'
import { InvalidRequestError } from '../oauth-errors.js'
import { Sub } from '../oidc/sub.js'
import { ClientAuth } from '../token/token-store.js'
import { constantTime } from '../util/time.js'
import { AccountInfo, AccountStore, LoginCredentials } from './account-store.js'
import { AccountHooks } from './account-hooks.js'
import {
Account,
AccountInfo,
AccountStore,
LoginCredentials,
} from './account-store.js'

const TIMING_ATTACK_MITIGATION_DELAY = 400

export class AccountManager {
constructor(protected readonly store: AccountStore) {}
constructor(
protected readonly store: AccountStore,
protected readonly hooks: AccountHooks,
) {}

public async signIn(
credentials: LoginCredentials,
Expand All @@ -31,10 +41,20 @@ export class AccountManager {

public async addAuthorizedClient(
deviceId: DeviceId,
sub: Sub,
clientId: OAuthClientId,
account: Account,
client: Client,
clientAuth: ClientAuth,
): Promise<void> {
await this.store.addAuthorizedClient(deviceId, sub, clientId)
if (this.hooks.onAccountAddAuthorizedClient) {
const shouldAdd = await this.hooks.onAccountAddAuthorizedClient(client, {
deviceId,
account,
clientAuth,
})
if (!shouldAdd) return
}

await this.store.addAuthorizedClient(deviceId, account.sub, client.id)
}

public async list(deviceId: DeviceId): Promise<AccountInfo[]> {
Expand Down
2 changes: 1 addition & 1 deletion packages/oauth-provider/src/account/account-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export type DeviceAccountInfo = {
}

// Export all types needed to implement the AccountStore interface
export type { Awaitable, Account, DeviceId, Sub }
export type { Account, DeviceId, Sub }

export type AccountInfo = {
account: Account
Expand Down
22 changes: 22 additions & 0 deletions packages/oauth-provider/src/client/client-hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Jwks } from '@atproto/jwk'
import {
OAuthClientId,
OAuthClientMetadata,
} from '@atproto/oauth-client-metadata'
import { Awaitable } from '../util/awaitable.js'

/**
* Use this to alter, override or validate the client metadata & jwks returned
* by the client store.
*
* @throws {InvalidClientMetadataError} if the metadata is invalid
* @see {@link InvalidClientMetadataError}
*/
export type ClientDataHook = (
clientId: OAuthClientId,
data: { metadata: OAuthClientMetadata; jwks?: Jwks },
) => Awaitable<void>

export type ClientHooks = {
onClientData?: ClientDataHook
}
23 changes: 4 additions & 19 deletions packages/oauth-provider/src/client/client-manager.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,20 @@
import { Jwks, Keyset } from '@atproto/jwk'
import {
OAuthClientId,
OAuthClientMetadata,
} from '@atproto/oauth-client-metadata'
import { Keyset } from '@atproto/jwk'
import { OAuthClientId } from '@atproto/oauth-client-metadata'

import { InvalidClientMetadataError } from '../errors/invalid-client-metadata-error.js'
import { InvalidRedirectUriError } from '../errors/invalid-redirect-uri-error.js'
import { OAuthError } from '../errors/oauth-error.js'
import { Awaitable } from '../util/awaitable.js'
import { ClientData } from './client-data.js'
import { ClientHooks } from './client-hooks.js'
import { ClientStore } from './client-store.js'
import { parseRedirectUri } from './client-utils.js'
import { Client } from './client.js'

/**
* Use this to alter, override or validate the client metadata & jwks returned
* by the client store.
*
* @throws {InvalidClientMetadataError} if the metadata is invalid
* @see {@link InvalidClientMetadataError}
*/
export type ClientDataHook = (
clientId: OAuthClientId,
data: { metadata: OAuthClientMetadata; jwks?: Jwks },
) => Awaitable<void>

export class ClientManager {
constructor(
protected readonly store: ClientStore,
protected readonly keyset: Keyset,
protected readonly hooks: { onClientData?: ClientDataHook },
protected readonly hooks: ClientHooks,
) {}

/**
Expand Down
14 changes: 10 additions & 4 deletions packages/oauth-provider/src/oauth-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@
* This file exposes all the hooks that can be used when instantiating the
* OAuthProvider.
*/
import type { AccountHooks } from './account/account-hooks.js'
import type { ClientHooks } from './client/client-hooks.js'
import type { RequestHooks } from './request/request-hooks.js'
import type { TokenHooks } from './token/token-hooks.js'

export { type AuthorizationDetailsHook } from './token/token-manager.js'
export { type ClientDataHook } from './client/client-manager.js'
export { type TokenResponseHook } from './token/token-manager.js'
export { type AuthorizationRequestHook } from './request/request-manager.js'
export type * from './account/account-hooks.js'
export type * from './client/client-hooks.js'
export type * from './request/request-hooks.js'
export type * from './token/token-hooks.js'

export type OAuthHooks = AccountHooks & ClientHooks & RequestHooks & TokenHooks

0 comments on commit c4d53b7

Please sign in to comment.