feat(frontend): hosted sign-in and OAuth settings for Cognito app clients
CI / checks (pull_request) Successful in 26m7s

This commit is contained in:
Zakaria
2026-10-24 11:16:46 -04:00
parent d411f632c9
commit 238bb1a3d5
16 changed files with 522 additions and 3 deletions
@@ -23,6 +23,7 @@ import {
ListUserPoolClientsCommand,
ListUserPoolsCommand,
ListUsersCommand,
UpdateUserPoolClientCommand,
type CognitoIdentityProviderClient,
} from '@aws-sdk/client-cognito-identity-provider'
import {AwsUserPoolsAdapter} from './AwsUserPoolsAdapter'
@@ -409,3 +410,63 @@ describe('AwsUserPoolsAdapter user actions and groups', () => {
.toEqual({outcome: 'challenge', challengeName: 'NEW_PASSWORD_REQUIRED'})
})
})
describe('app client OAuth settings', () => {
const CLIENT_ID = '40808d60e188483d876bbbf646'
const stored = {
UserPoolId: POOL_ID, ClientId: CLIENT_ID, ClientName: 'notes-demo-web',
ExplicitAuthFlows: ['ALLOW_USER_PASSWORD_AUTH', 'ALLOW_REFRESH_TOKEN_AUTH'],
PreventUserExistenceErrors: 'ENABLED', RefreshTokenValidity: 30, EnableTokenRevocation: true,
SupportedIdentityProviders: ['COGNITO'],
}
function clientStub() {
return stubCognito((command) => {
if (command instanceof DescribeUserPoolClientCommand) return {UserPoolClient: stored}
if (command instanceof UpdateUserPoolClientCommand) return {UserPoolClient: {...stored, ...command.input}}
return {}
})
}
test('reads a client with no OAuth set up', async () => {
const settings = await new AwsUserPoolsAdapter(clientStub().client).describeUserPoolClient(POOL_ID, CLIENT_ID)
expect(settings).toEqual({
clientId: CLIENT_ID, clientName: 'notes-demo-web', hasSecret: false,
explicitAuthFlows: ['ALLOW_USER_PASSWORD_AUTH', 'ALLOW_REFRESH_TOKEN_AUTH'],
callbackUrls: [], logoutUrls: [], oauthFlows: [], oauthScopes: [], identityProviders: ['COGNITO'],
})
})
test('turns on the code flow and sends the client\'s other settings back unchanged', async () => {
const {client, sent} = clientStub()
const settings = await new AwsUserPoolsAdapter(client).updateUserPoolClientOAuth(POOL_ID, CLIENT_ID, {
callbackUrls: ['http://localhost:4600/api/auth/callback', ' http://localhost:4600/api/auth/callback '],
logoutUrls: ['http://localhost:4600/'],
oauthFlows: ['code'], oauthScopes: ['openid', 'email'], identityProviders: ['COGNITO'],
})
const update = sent.find((command) => command instanceof UpdateUserPoolClientCommand) as UpdateUserPoolClientCommand
expect(update.input).toMatchObject({
ClientName: 'notes-demo-web', ExplicitAuthFlows: ['ALLOW_USER_PASSWORD_AUTH', 'ALLOW_REFRESH_TOKEN_AUTH'],
PreventUserExistenceErrors: 'ENABLED', RefreshTokenValidity: 30, EnableTokenRevocation: true,
CallbackURLs: ['http://localhost:4600/api/auth/callback'], LogoutURLs: ['http://localhost:4600/'],
AllowedOAuthFlows: ['code'], AllowedOAuthScopes: ['openid', 'email'], AllowedOAuthFlowsUserPoolClient: true,
SupportedIdentityProviders: ['COGNITO'],
})
expect(settings.oauthFlows).toEqual(['code'])
})
test('applies Cognito\'s rules before calling it', async () => {
const {client, sent} = clientStub()
const adapter = new AwsUserPoolsAdapter(client)
const valid = {callbackUrls: ['https://app.example/callback'], logoutUrls: [], oauthFlows: ['code' as const], oauthScopes: ['openid'], identityProviders: ['COGNITO']}
await expect(adapter.updateUserPoolClientOAuth(POOL_ID, CLIENT_ID, {...valid, callbackUrls: ['http://app.example/callback']})).rejects.toThrow(/https/)
await expect(adapter.updateUserPoolClientOAuth(POOL_ID, CLIENT_ID, {...valid, callbackUrls: ['https://app.example/#done']})).rejects.toThrow(/fragment/)
await expect(adapter.updateUserPoolClientOAuth(POOL_ID, CLIENT_ID, {...valid, callbackUrls: []})).rejects.toThrow(/callback URL/)
await expect(adapter.updateUserPoolClientOAuth(POOL_ID, CLIENT_ID, {...valid, oauthScopes: []})).rejects.toThrow(/scope/)
await expect(adapter.updateUserPoolClientOAuth(POOL_ID, CLIENT_ID, {...valid, oauthFlows: ['code', 'client_credentials']})).rejects.toThrow(ValidationError)
await expect(adapter.updateUserPoolClientOAuth(POOL_ID, CLIENT_ID, {...valid, oauthFlows: ['client_credentials']})).rejects.toThrow(/secret/)
expect(sent.some((command) => command instanceof UpdateUserPoolClientCommand)).toBe(false)
})
})