Improve login_hint handling (#3933)

* Return atproto handle in identity resolution result

* Use resolved handle or did instead of raw input as "login_hint"

* Normalize and validate `login_hint` in oauth request properties
This commit is contained in:
Matthieu Sieben 2025-06-10 11:57:49 +02:00 committed by GitHub
parent 4e96e2c7b7
commit 192f3ab89c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 84 additions and 128 deletions

View File

@ -0,0 +1,5 @@
---
"@atproto/oauth-client": patch
---
Use resolved handle or did instead of raw input as "login_hint"

View File

@ -0,0 +1,5 @@
---
"@atproto/oauth-provider": patch
---
Normalize and validate `login_hint` in oauth request properties

View File

@ -0,0 +1,5 @@
---
"@atproto-labs/identity-resolver": patch
---
Return atproto handle in identity resolution result

View File

@ -1,4 +1,4 @@
import { normalizeAndEnsureValidHandle } from '@atproto/syntax'
import { isValidHandle, normalizeAndEnsureValidHandle } from '@atproto/syntax'
import {
Did,
DidDocument,
@ -17,6 +17,7 @@ import {
export type ResolvedIdentity = {
did: NonNullable<ResolvedHandle>
pds: URL
handle?: string
}
export type ResolveIdentityOptions = ResolveDidOptions & ResolveHandleOptions
@ -49,6 +50,7 @@ export class IdentityResolver {
return {
did: document.id,
pds: new URL(service.serviceEndpoint),
handle: extractHandle(document),
}
}
@ -89,6 +91,21 @@ export class IdentityResolver {
}
}
function extractHandle(
document: DidDocument<AtprotoIdentityDidMethods>,
): string | undefined {
if (document.alsoKnownAs) {
for (const h of document.alsoKnownAs) {
if (h.startsWith('at://')) {
const handle = h.slice(5)
if (isValidHandle(handle)) return handle
}
}
}
return undefined
}
function isAtprotoPersonalDataServerService<M extends string>(
this: DidDocument<M>,
s: DidService,

View File

@ -307,9 +307,7 @@ export class OAuthClient extends CustomEventTarget<OAuthClientEventMap> {
code_challenge: pkce.challenge,
code_challenge_method: pkce.method,
state,
login_hint: identity
? input // If input is a handle or a DID, use it as a login_hint
: undefined,
login_hint: identity?.handle ?? identity?.did,
response_mode: this.responseMode,
response_type: 'code' as const,
scope: options?.scope ?? this.clientMetadata.scope,

View File

@ -37,6 +37,7 @@
"@atproto-labs/simple-store": "workspace:*",
"@atproto-labs/simple-store-memory": "workspace:*",
"@atproto/common": "workspace:^",
"@atproto/did": "workspace:*",
"@atproto/jwk": "workspace:*",
"@atproto/jwk-jose": "workspace:*",
"@atproto/oauth-types": "workspace:*",

View File

@ -1,9 +1,11 @@
import { isAtprotoDid } from '@atproto/did'
import type { Account } from '@atproto/oauth-provider-api'
import {
CLIENT_ASSERTION_TYPE_JWT_BEARER,
OAuthAuthorizationRequestParameters,
OAuthAuthorizationServerMetadata,
} from '@atproto/oauth-types'
import { isValidHandle } from '@atproto/syntax'
import { ClientAuth } from '../client/client-auth.js'
import { ClientId } from '../client/client-id.js'
import { Client } from '../client/client.js'
@ -305,6 +307,24 @@ export class RequestManager {
parameters = { ...parameters, prompt: 'consent' }
}
// atproto extension: ensure that the login_hint is a valid handle or DID
// @NOTE we to allow invalid case here, which is not spec'd anywhere.
const hint = parameters.login_hint?.toLowerCase()
if (hint) {
if (!isAtprotoDid(hint) && !isValidHandle(hint)) {
throw new InvalidParametersError(
parameters,
`Invalid login_hint "${hint}"`,
)
}
// @TODO: ensure that the account actually exists on this server (there is
// no point in showing the UI to the user if the account does not exist).
// Update the parameters to ensure the right case is used
parameters = { ...parameters, login_hint: hint }
}
return parameters
}

153
pnpm-lock.yaml generated
View File

@ -58,7 +58,7 @@ importers:
version: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5)
jest:
specifier: ^28.1.2
version: 28.1.2(@types/node@18.19.67)
version: 28.1.2(@types/node@18.19.67)(ts-node@10.8.2)
node-gyp:
specifier: ^9.3.1
version: 9.3.1
@ -110,7 +110,7 @@ importers:
version: link:../lex-cli
jest:
specifier: ^28.1.2
version: 28.1.2(@types/node@18.19.67)
version: 28.1.2(@types/node@18.19.67)(ts-node@10.8.2)
prettier:
specifier: ^3.2.5
version: 3.2.5
@ -319,10 +319,10 @@ importers:
version: 6.9.7
jest:
specifier: ^28.1.2
version: 28.1.2(@types/node@22.15.27)(ts-node@10.8.2)
version: 28.1.2(@types/node@18.19.67)(ts-node@10.8.2)
ts-node:
specifier: ^10.8.2
version: 10.8.2(@swc/core@1.11.18)(@types/node@22.15.27)(typescript@5.8.2)
version: 10.8.2(@swc/core@1.11.18)(@types/node@18.19.67)(typescript@5.8.2)
typescript:
specifier: ^5.6.3
version: 5.8.2
@ -377,10 +377,10 @@ importers:
version: 5.1.1
jest:
specifier: ^28.1.2
version: 28.1.2(@types/node@22.15.27)(ts-node@10.8.2)
version: 28.1.2(@types/node@18.19.67)(ts-node@10.8.2)
ts-node:
specifier: ^10.8.2
version: 10.8.2(@swc/core@1.11.18)(@types/node@22.15.27)(typescript@5.8.2)
version: 10.8.2(@swc/core@1.11.18)(@types/node@18.19.67)(typescript@5.8.2)
typescript:
specifier: ^5.6.3
version: 5.8.2
@ -408,7 +408,7 @@ importers:
devDependencies:
jest:
specifier: ^28.1.2
version: 28.1.2(@types/node@18.19.67)
version: 28.1.2(@types/node@18.19.67)(ts-node@10.8.2)
typescript:
specifier: ^5.6.3
version: 5.8.2
@ -433,7 +433,7 @@ importers:
devDependencies:
jest:
specifier: ^28.1.2
version: 28.1.2(@types/node@18.19.67)
version: 28.1.2(@types/node@18.19.67)(ts-node@10.8.2)
typescript:
specifier: ^5.6.3
version: 5.8.2
@ -455,7 +455,7 @@ importers:
version: link:../common
jest:
specifier: ^28.1.2
version: 28.1.2(@types/node@18.19.67)
version: 28.1.2(@types/node@18.19.67)(ts-node@10.8.2)
typescript:
specifier: ^5.6.3
version: 5.8.2
@ -541,7 +541,7 @@ importers:
version: 0.2.24(@swc/core@1.11.29)
jest:
specifier: ^28.1.2
version: 28.1.2(@types/node@18.19.67)
version: 28.1.2(@types/node@18.19.67)(ts-node@10.8.2)
typescript:
specifier: ^5.6.3
version: 5.8.2
@ -572,7 +572,7 @@ importers:
version: 6.1.2
jest:
specifier: ^28.1.2
version: 28.1.2(@types/node@18.19.67)
version: 28.1.2(@types/node@18.19.67)(ts-node@10.8.2)
typescript:
specifier: ^5.6.3
version: 5.8.2
@ -784,7 +784,7 @@ importers:
devDependencies:
jest:
specifier: ^28.1.2
version: 28.1.2(@types/node@18.19.67)
version: 28.1.2(@types/node@18.19.67)(ts-node@10.8.2)
typescript:
specifier: ^5.6.3
version: 5.8.2
@ -1037,6 +1037,9 @@ importers:
'@atproto/common':
specifier: workspace:^
version: link:../../common
'@atproto/did':
specifier: workspace:*
version: link:../../did
'@atproto/jwk':
specifier: workspace:*
version: link:../jwk
@ -1399,10 +1402,10 @@ importers:
version: 6.9.7
jest:
specifier: ^28.1.2
version: 28.1.2(@types/node@22.15.27)(ts-node@10.8.2)
version: 28.1.2(@types/node@18.19.67)(ts-node@10.8.2)
ts-node:
specifier: ^10.8.2
version: 10.8.2(@swc/core@1.11.18)(@types/node@22.15.27)(typescript@5.8.2)
version: 10.8.2(@swc/core@1.11.18)(@types/node@18.19.67)(typescript@5.8.2)
typescript:
specifier: ^5.6.3
version: 5.8.2
@ -1577,13 +1580,13 @@ importers:
version: 6.1.2
jest:
specifier: ^28.1.2
version: 28.1.2(@types/node@22.15.27)(ts-node@10.8.2)
version: 28.1.2(@types/node@18.19.67)(ts-node@10.8.2)
puppeteer:
specifier: ^23.5.2
version: 23.5.3(typescript@5.8.2)
ts-node:
specifier: ^10.8.2
version: 10.8.2(@swc/core@1.11.18)(@types/node@22.15.27)(typescript@5.8.2)
version: 10.8.2(@swc/core@1.11.18)(@types/node@18.19.67)(typescript@5.8.2)
typescript:
specifier: ^5.6.3
version: 5.8.2
@ -1623,7 +1626,7 @@ importers:
devDependencies:
jest:
specifier: ^28.1.2
version: 28.1.2(@types/node@18.19.67)
version: 28.1.2(@types/node@18.19.67)(ts-node@10.8.2)
typescript:
specifier: ^5.6.3
version: 5.8.2
@ -1663,7 +1666,7 @@ importers:
version: 8.5.4
jest:
specifier: ^28.1.2
version: 28.1.2(@types/node@18.19.67)
version: 28.1.2(@types/node@18.19.67)(ts-node@10.8.2)
typescript:
specifier: ^5.6.3
version: 5.8.2
@ -1672,7 +1675,7 @@ importers:
devDependencies:
jest:
specifier: ^28.1.2
version: 28.1.2(@types/node@18.19.67)
version: 28.1.2(@types/node@18.19.67)(ts-node@10.8.2)
typescript:
specifier: ^5.6.3
version: 5.8.2
@ -1746,7 +1749,7 @@ importers:
version: 6.1.2
jest:
specifier: ^28.1.2
version: 28.1.2(@types/node@18.19.67)
version: 28.1.2(@types/node@18.19.67)(ts-node@10.8.2)
jose:
specifier: ^4.15.4
version: 4.15.4
@ -8584,12 +8587,6 @@ packages:
dependencies:
undici-types: 5.26.5
/@types/node@22.15.27:
resolution: {integrity: sha512-5fF+eu5mwihV2BeVtX5vijhdaZOfkQTATrePEaXTcKqI16LhJ7gi2/Vhd9OZM0UojcdmiOCVg5rrax+i1MdoQQ==}
dependencies:
undici-types: 6.21.0
dev: true
/@types/nodemailer@6.4.6:
resolution: {integrity: sha512-pD6fL5GQtUKvD2WnPmg5bC2e8kWCAPDwMPmHe/ohQbW+Dy0EcHgZ2oCSuPlWNqk74LS5BVMig1SymQbFMPPK3w==}
dependencies:
@ -13161,7 +13158,7 @@ packages:
- supports-color
dev: true
/jest-cli@28.1.3(@types/node@18.19.67):
/jest-cli@28.1.3(@types/node@18.19.67)(ts-node@10.8.2):
resolution: {integrity: sha512-roY3kvrv57Azn1yPgdTebPAXvdR2xfezaKKYzVxZ6It/5NCxzJym6tUI5P1zkdWhfUYkxEI9uZWcQdaFLo8mJQ==}
engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0}
hasBin: true
@ -13189,34 +13186,6 @@ packages:
- ts-node
dev: true
/jest-cli@28.1.3(@types/node@22.15.27)(ts-node@10.8.2):
resolution: {integrity: sha512-roY3kvrv57Azn1yPgdTebPAXvdR2xfezaKKYzVxZ6It/5NCxzJym6tUI5P1zkdWhfUYkxEI9uZWcQdaFLo8mJQ==}
engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0}
hasBin: true
peerDependencies:
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
peerDependenciesMeta:
node-notifier:
optional: true
dependencies:
'@jest/core': 28.1.3(ts-node@10.8.2)
'@jest/test-result': 28.1.3
'@jest/types': 28.1.3
chalk: 4.1.2
exit: 0.1.2
graceful-fs: 4.2.11
import-local: 3.1.0
jest-config: 28.1.3(@types/node@22.15.27)(ts-node@10.8.2)
jest-util: 28.1.3
jest-validate: 28.1.3
prompts: 2.4.2
yargs: 17.7.2
transitivePeerDependencies:
- '@types/node'
- supports-color
- ts-node
dev: true
/jest-config@28.1.3(@types/node@18.19.67)(ts-node@10.8.2):
resolution: {integrity: sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ==}
engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0}
@ -13252,47 +13221,7 @@ packages:
pretty-format: 28.1.3
slash: 3.0.0
strip-json-comments: 3.1.1
ts-node: 10.8.2(@swc/core@1.11.18)(@types/node@22.15.27)(typescript@5.8.2)
transitivePeerDependencies:
- supports-color
dev: true
/jest-config@28.1.3(@types/node@22.15.27)(ts-node@10.8.2):
resolution: {integrity: sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ==}
engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0}
peerDependencies:
'@types/node': '*'
ts-node: '>=9.0.0'
peerDependenciesMeta:
'@types/node':
optional: true
ts-node:
optional: true
dependencies:
'@babel/core': 7.18.6
'@jest/test-sequencer': 28.1.3
'@jest/types': 28.1.3
'@types/node': 22.15.27
babel-jest: 28.1.3(@babel/core@7.18.6)
chalk: 4.1.2
ci-info: 3.8.0
deepmerge: 4.3.1
glob: 7.2.3
graceful-fs: 4.2.11
jest-circus: 28.1.3
jest-environment-node: 28.1.3
jest-get-type: 28.0.2
jest-regex-util: 28.0.2
jest-resolve: 28.1.3
jest-runner: 28.1.3
jest-util: 28.1.3
jest-validate: 28.1.3
micromatch: 4.0.5
parse-json: 5.2.0
pretty-format: 28.1.3
slash: 3.0.0
strip-json-comments: 3.1.1
ts-node: 10.8.2(@swc/core@1.11.18)(@types/node@22.15.27)(typescript@5.8.2)
ts-node: 10.8.2(@swc/core@1.11.18)(@types/node@18.19.67)(typescript@5.8.2)
transitivePeerDependencies:
- supports-color
dev: true
@ -13605,7 +13534,7 @@ packages:
supports-color: 8.1.1
dev: true
/jest@28.1.2(@types/node@18.19.67):
/jest@28.1.2(@types/node@18.19.67)(ts-node@10.8.2):
resolution: {integrity: sha512-Tuf05DwLeCh2cfWCQbcz9UxldoDyiR1E9Igaei5khjonKncYdc6LDfynKCEWozK0oLE3GD+xKAo2u8x/0s6GOg==}
engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0}
hasBin: true
@ -13618,27 +13547,7 @@ packages:
'@jest/core': 28.1.3(ts-node@10.8.2)
'@jest/types': 28.1.3
import-local: 3.1.0
jest-cli: 28.1.3(@types/node@18.19.67)
transitivePeerDependencies:
- '@types/node'
- supports-color
- ts-node
dev: true
/jest@28.1.2(@types/node@22.15.27)(ts-node@10.8.2):
resolution: {integrity: sha512-Tuf05DwLeCh2cfWCQbcz9UxldoDyiR1E9Igaei5khjonKncYdc6LDfynKCEWozK0oLE3GD+xKAo2u8x/0s6GOg==}
engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0}
hasBin: true
peerDependencies:
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
peerDependenciesMeta:
node-notifier:
optional: true
dependencies:
'@jest/core': 28.1.3(ts-node@10.8.2)
'@jest/types': 28.1.3
import-local: 3.1.0
jest-cli: 28.1.3(@types/node@22.15.27)(ts-node@10.8.2)
jest-cli: 28.1.3(@types/node@18.19.67)(ts-node@10.8.2)
transitivePeerDependencies:
- '@types/node'
- supports-color
@ -17429,7 +17338,7 @@ packages:
code-block-writer: 13.0.3
dev: false
/ts-node@10.8.2(@swc/core@1.11.18)(@types/node@22.15.27)(typescript@5.8.2):
/ts-node@10.8.2(@swc/core@1.11.18)(@types/node@18.19.67)(typescript@5.8.2):
resolution: {integrity: sha512-LYdGnoGddf1D6v8REPtIH+5iq/gTDuZqv2/UJUU7tKjuEU8xVZorBM+buCGNjj+pGEud+sOoM4CX3/YzINpENA==}
hasBin: true
peerDependencies:
@ -17449,7 +17358,7 @@ packages:
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4
'@types/node': 22.15.27
'@types/node': 18.19.67
acorn: 8.10.0
acorn-walk: 8.2.0
arg: 4.1.3
@ -17746,10 +17655,6 @@ packages:
/undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
/undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
dev: true
/undici@5.28.4:
resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==}
engines: {node: '>=14.0'}