+96
-26
src/pds.js
+96
-26
src/pds.js
···
3903
3903
3904
3904
/**
3905
3905
* Handle GET /oauth/authorize - displays the consent UI.
3906
-
* Validates the request_uri from PAR and renders a login/consent form.
3906
+
* Supports both PAR (request_uri) and direct authorization parameters.
3907
3907
* @param {URL} url - Parsed request URL
3908
3908
* @returns {Promise<Response>} HTML consent page
3909
3909
*/
3910
3910
async handleOAuthAuthorizeGet(url) {
3911
+
// Opportunistically clean up expired authorization requests
3912
+
this.cleanupExpiredAuthorizationRequests();
3913
+
3911
3914
const requestUri = url.searchParams.get('request_uri');
3912
3915
const clientId = url.searchParams.get('client_id');
3913
3916
3914
-
if (!requestUri || !clientId) {
3915
-
return new Response('Missing parameters', { status: 400 });
3917
+
// If request_uri is present, use PAR flow
3918
+
if (requestUri) {
3919
+
if (!clientId) {
3920
+
return new Response('Missing client_id parameter', { status: 400 });
3921
+
}
3922
+
3923
+
const match = requestUri.match(/^urn:ietf:params:oauth:request_uri:(.+)$/);
3924
+
if (!match) return new Response('Invalid request_uri', { status: 400 });
3925
+
3926
+
const rows = this.sql
3927
+
.exec(
3928
+
`SELECT * FROM authorization_requests WHERE id = ? AND client_id = ?`,
3929
+
match[1],
3930
+
clientId,
3931
+
)
3932
+
.toArray();
3933
+
const authRequest = rows[0];
3934
+
3935
+
if (!authRequest) return new Response('Request not found', { status: 400 });
3936
+
if (new Date(/** @type {string} */ (authRequest.expires_at)) < new Date())
3937
+
return new Response('Request expired', { status: 400 });
3938
+
if (authRequest.code)
3939
+
return new Response('Request already used', { status: 400 });
3940
+
3941
+
const clientMetadata = JSON.parse(
3942
+
/** @type {string} */ (authRequest.client_metadata),
3943
+
);
3944
+
const parameters = JSON.parse(
3945
+
/** @type {string} */ (authRequest.parameters),
3946
+
);
3947
+
3948
+
return new Response(
3949
+
renderConsentPage({
3950
+
clientName: clientMetadata.client_name || clientId,
3951
+
clientId: clientId || '',
3952
+
scope: parameters.scope || 'atproto',
3953
+
requestUri: requestUri || '',
3954
+
}),
3955
+
{ status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } },
3956
+
);
3916
3957
}
3917
3958
3918
-
const match = requestUri.match(/^urn:ietf:params:oauth:request_uri:(.+)$/);
3919
-
if (!match) return new Response('Invalid request_uri', { status: 400 });
3959
+
// Direct authorization flow - create request on-the-fly
3960
+
if (!clientId) {
3961
+
return new Response('Missing client_id parameter', { status: 400 });
3962
+
}
3920
3963
3921
-
const rows = this.sql
3922
-
.exec(
3923
-
`SELECT * FROM authorization_requests WHERE id = ? AND client_id = ?`,
3924
-
match[1],
3925
-
clientId,
3926
-
)
3927
-
.toArray();
3928
-
const authRequest = rows[0];
3964
+
const redirectUri = url.searchParams.get('redirect_uri');
3965
+
const responseType = url.searchParams.get('response_type');
3966
+
const responseMode = url.searchParams.get('response_mode');
3967
+
const scope = url.searchParams.get('scope');
3968
+
const state = url.searchParams.get('state');
3969
+
const codeChallenge = url.searchParams.get('code_challenge');
3970
+
const codeChallengeMethod = url.searchParams.get('code_challenge_method');
3971
+
const loginHint = url.searchParams.get('login_hint');
3929
3972
3930
-
if (!authRequest) return new Response('Request not found', { status: 400 });
3931
-
if (new Date(/** @type {string} */ (authRequest.expires_at)) < new Date())
3932
-
return new Response('Request expired', { status: 400 });
3933
-
if (authRequest.code)
3934
-
return new Response('Request already used', { status: 400 });
3973
+
// Validate parameters using shared helper
3974
+
const validationResult = await this.validateAuthorizationParameters({
3975
+
clientId,
3976
+
redirectUri,
3977
+
responseType,
3978
+
codeChallenge,
3979
+
codeChallengeMethod,
3980
+
});
3981
+
if ('error' in validationResult) return validationResult.error;
3982
+
const { clientMetadata } = validationResult;
3983
+
3984
+
// Create authorization request record (same as PAR but without DPoP)
3985
+
const requestId = crypto.randomUUID();
3986
+
const newRequestUri = `urn:ietf:params:oauth:request_uri:${requestId}`;
3987
+
const expiresIn = 600;
3988
+
const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
3935
3989
3936
-
const clientMetadata = JSON.parse(
3937
-
/** @type {string} */ (authRequest.client_metadata),
3938
-
);
3939
-
const parameters = JSON.parse(
3940
-
/** @type {string} */ (authRequest.parameters),
3990
+
this.sql.exec(
3991
+
`INSERT INTO authorization_requests (
3992
+
id, client_id, client_metadata, parameters,
3993
+
code_challenge, code_challenge_method, dpop_jkt,
3994
+
expires_at, created_at
3995
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3996
+
requestId,
3997
+
clientId,
3998
+
JSON.stringify(clientMetadata),
3999
+
JSON.stringify({
4000
+
redirect_uri: redirectUri,
4001
+
scope,
4002
+
state,
4003
+
response_mode: responseMode,
4004
+
login_hint: loginHint,
4005
+
}),
4006
+
codeChallenge,
4007
+
codeChallengeMethod,
4008
+
null, // No DPoP for direct authorization - will be bound at token exchange
4009
+
expiresAt,
4010
+
new Date().toISOString(),
3941
4011
);
3942
4012
3943
4013
return new Response(
3944
4014
renderConsentPage({
3945
4015
clientName: clientMetadata.client_name || clientId,
3946
-
clientId: clientId || '',
3947
-
scope: parameters.scope || 'atproto',
3948
-
requestUri: requestUri || '',
4016
+
clientId: clientId,
4017
+
scope: scope || 'atproto',
4018
+
requestUri: newRequestUri,
3949
4019
}),
3950
4020
{ status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } },
3951
4021
);