oauth2 tokens
This commit is contained in:
parent
6222b7ba18
commit
610b3a8c09
@ -4,4 +4,4 @@ import { handleSession } from 'svelte-kit-cookie-session';
|
|||||||
|
|
||||||
export const handle = handleSession({
|
export const handle = handleSession({
|
||||||
secret: SESSION_SECRET
|
secret: SESSION_SECRET
|
||||||
})
|
});
|
||||||
|
8
src/lib/server/api-utils.ts
Normal file
8
src/lib/server/api-utils.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export class ApiUtils {
|
||||||
|
static json(data: unknown, status = 200): Response {
|
||||||
|
return new Response(JSON.stringify(data), {
|
||||||
|
status,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -69,11 +69,13 @@ export class CryptoUtils {
|
|||||||
return crypto.createHash('sha256').update(input).digest();
|
return crypto.createHash('sha256').update(input).digest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static sanitizeS256(input: string) {
|
||||||
|
return input.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
static createS256(input: string) {
|
static createS256(input: string) {
|
||||||
return CryptoUtils.sha256hash(Buffer.from(input).toString('ascii'))
|
return CryptoUtils.sanitizeS256(
|
||||||
.toString('base64')
|
CryptoUtils.sha256hash(Buffer.from(input).toString('ascii')).toString('base64')
|
||||||
.replace(/\+/g, '-')
|
);
|
||||||
.replace(/\//g, '_')
|
|
||||||
.replace(/=+$/, '');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1
src/lib/server/oauth2/.gitignore
vendored
1
src/lib/server/oauth2/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
impl.reference/
|
|
@ -1,4 +1,4 @@
|
|||||||
import type { UserSession } from '../users';
|
import type { UserSession } from '../../users';
|
||||||
import {
|
import {
|
||||||
InvalidRequest,
|
InvalidRequest,
|
||||||
UnsupportedResponseType,
|
UnsupportedResponseType,
|
||||||
@ -8,25 +8,25 @@ import {
|
|||||||
AccessDenied,
|
AccessDenied,
|
||||||
InvalidGrant,
|
InvalidGrant,
|
||||||
InteractionRequired
|
InteractionRequired
|
||||||
} from './error';
|
} from '../error';
|
||||||
import { OAuth2AccessTokens, OAuth2Clients, OAuth2Codes, OAuth2Tokens } from './model';
|
import {
|
||||||
import { OAuth2Users } from './model/user';
|
OAuth2AccessTokens,
|
||||||
import { OAuth2Response } from './response';
|
OAuth2Clients,
|
||||||
|
OAuth2Codes,
|
||||||
|
OAuth2Tokens,
|
||||||
|
type CodeChallengeMethod
|
||||||
|
} from '../model';
|
||||||
|
import { OAuth2Users } from '../model/user';
|
||||||
|
import { OAuth2Response } from '../response';
|
||||||
|
|
||||||
export class OAuth2Authorization {
|
export class OAuth2AuthorizationController {
|
||||||
static prehandle = async (url: URL, locals: App.Locals) => {
|
static prehandle = async (url: URL, locals: App.Locals) => {
|
||||||
let clientId: string | null = null;
|
|
||||||
let redirectUri: string | null = null;
|
|
||||||
let responseType: string | null = null;
|
|
||||||
let grantTypes: string[] = [];
|
|
||||||
let scope: string[] | null = null;
|
|
||||||
|
|
||||||
if (!url.searchParams.has('redirect_uri')) {
|
if (!url.searchParams.has('redirect_uri')) {
|
||||||
throw new InvalidRequest('redirect_uri field is mandatory for authorization endpoint');
|
throw new InvalidRequest('redirect_uri field is mandatory for authorization endpoint');
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectUri = url.searchParams.get('redirect_uri') as string;
|
const redirectUri = url.searchParams.get('redirect_uri') as string;
|
||||||
// req.oauth2.logger.debug('Parameter redirect uri is', redirectUri);
|
// console.debug('Parameter redirect uri is', redirectUri);
|
||||||
|
|
||||||
if (!url.searchParams.has('client_id')) {
|
if (!url.searchParams.has('client_id')) {
|
||||||
throw new InvalidRequest('client_id field is mandatory for authorization endpoint');
|
throw new InvalidRequest('client_id field is mandatory for authorization endpoint');
|
||||||
@ -39,18 +39,19 @@ export class OAuth2Authorization {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
clientId = url.searchParams.get('client_id') as string;
|
const clientId = url.searchParams.get('client_id') as string;
|
||||||
// req.oauth2.logger.debug('Parameter client_id is', clientId);
|
// console.debug('Parameter client_id is', clientId);
|
||||||
|
|
||||||
if (!url.searchParams.has('response_type')) {
|
if (!url.searchParams.has('response_type')) {
|
||||||
throw new InvalidRequest('response_type field is mandatory for authorization endpoint');
|
throw new InvalidRequest('response_type field is mandatory for authorization endpoint');
|
||||||
}
|
}
|
||||||
|
|
||||||
responseType = url.searchParams.get('response_type') as string;
|
const responseType = url.searchParams.get('response_type') as string;
|
||||||
// req.oauth2.logger.debug('Parameter response_type is', responseType);
|
// console.debug('Parameter response_type is', responseType);
|
||||||
|
|
||||||
// Support multiple types
|
// Support multiple types
|
||||||
const responseTypes = responseType.split(' ');
|
const responseTypes = responseType.split(' ');
|
||||||
|
let grantTypes: string[] = [];
|
||||||
for (const i in responseTypes) {
|
for (const i in responseTypes) {
|
||||||
switch (responseTypes[i]) {
|
switch (responseTypes[i]) {
|
||||||
case 'code':
|
case 'code':
|
||||||
@ -74,11 +75,11 @@ export class OAuth2Authorization {
|
|||||||
grantTypes = grantTypes.filter((value, index, self) => self.indexOf(value) === index);
|
grantTypes = grantTypes.filter((value, index, self) => self.indexOf(value) === index);
|
||||||
|
|
||||||
// "None" type cannot be combined with others
|
// "None" type cannot be combined with others
|
||||||
if (grantTypes.length > 1 && grantTypes.indexOf('none') !== -1) {
|
if (grantTypes.length > 1 && grantTypes.includes('none')) {
|
||||||
throw new InvalidRequest('Grant type "none" cannot be combined with other grant types');
|
throw new InvalidRequest('Grant type "none" cannot be combined with other grant types');
|
||||||
}
|
}
|
||||||
|
|
||||||
// req.oauth2.logger.debug('Parameter grant_type is', grantTypes.join(' '));
|
// console.debug('Parameter grant_type is', grantTypes.join(' '));
|
||||||
|
|
||||||
const client = await OAuth2Clients.fetchById(clientId);
|
const client = await OAuth2Clients.fetchById(clientId);
|
||||||
if (!client) {
|
if (!client) {
|
||||||
@ -90,7 +91,7 @@ export class OAuth2Authorization {
|
|||||||
} else if (!(await OAuth2Clients.checkRedirectUri(client, redirectUri))) {
|
} else if (!(await OAuth2Clients.checkRedirectUri(client, redirectUri))) {
|
||||||
throw new InvalidRequest('Wrong RedirectUri provided');
|
throw new InvalidRequest('Wrong RedirectUri provided');
|
||||||
}
|
}
|
||||||
// req.oauth2.logger.debug('redirect_uri check passed');
|
// console.debug('redirect_uri check passed');
|
||||||
|
|
||||||
// The client needs to support all grant types
|
// The client needs to support all grant types
|
||||||
for (const grantType of grantTypes) {
|
for (const grantType of grantTypes) {
|
||||||
@ -98,19 +99,19 @@ export class OAuth2Authorization {
|
|||||||
throw new UnauthorizedClient('This client does not support grant type ' + grantType);
|
throw new UnauthorizedClient('This client does not support grant type ' + grantType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// req.oauth2.logger.debug('Grant type check passed');
|
// console.debug('Grant type check passed');
|
||||||
|
|
||||||
scope = OAuth2Clients.transformScope(url.searchParams.get('scope') as string);
|
const scope = OAuth2Clients.transformScope(url.searchParams.get('scope') as string);
|
||||||
if (!OAuth2Clients.checkScope(client, scope)) {
|
if (!OAuth2Clients.checkScope(client, scope)) {
|
||||||
throw new InvalidScope('Client does not allow access to this scope');
|
throw new InvalidScope('Client does not allow access to this scope');
|
||||||
}
|
}
|
||||||
// req.oauth2.logger.debug('Scope check passed');
|
// console.debug('Scope check passed');
|
||||||
|
|
||||||
const codeChallenge = url.searchParams.get('code_challenge') as string;
|
const codeChallenge = url.searchParams.get('code_challenge') as string;
|
||||||
const codeChallengeMethod =
|
const codeChallengeMethod =
|
||||||
(url.searchParams.get('code_challenge_method') as 'plain' | 'S256') || 'plain';
|
(url.searchParams.get('code_challenge_method') as CodeChallengeMethod) || 'plain';
|
||||||
|
|
||||||
if (codeChallengeMethod && !['plain', 'S256'].includes(codeChallengeMethod)) {
|
if (codeChallengeMethod && !OAuth2Tokens.challengeMethods.includes(codeChallengeMethod)) {
|
||||||
throw new InvalidGrant('Invalid code challenge method');
|
throw new InvalidGrant('Invalid code challenge method');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,7 +138,7 @@ export class OAuth2Authorization {
|
|||||||
codeChallengeMethod,
|
codeChallengeMethod,
|
||||||
redirectUri,
|
redirectUri,
|
||||||
responseType
|
responseType
|
||||||
}: Awaited<ReturnType<typeof OAuth2Authorization.prehandle>>
|
}: Awaited<ReturnType<typeof OAuth2AuthorizationController.prehandle>>
|
||||||
) => {
|
) => {
|
||||||
let resObj: Record<string, string | number> = {};
|
let resObj: Record<string, string | number> = {};
|
||||||
for (const i in grantTypes) {
|
for (const i in grantTypes) {
|
||||||
@ -203,7 +204,7 @@ export class OAuth2Authorization {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static getRequest = async ({ locals, url }: { locals: App.Locals; url: URL }) => {
|
static getRequest = async ({ locals, url }: { locals: App.Locals; url: URL }) => {
|
||||||
const prehandle = await OAuth2Authorization.prehandle(url, locals);
|
const prehandle = await OAuth2AuthorizationController.prehandle(url, locals);
|
||||||
const { client, scope, user } = prehandle;
|
const { client, scope, user } = prehandle;
|
||||||
|
|
||||||
const prompt = ((url.searchParams.get('prompt') || '') as string).split(' ');
|
const prompt = ((url.searchParams.get('prompt') || '') as string).split(' ');
|
||||||
@ -230,7 +231,7 @@ export class OAuth2Authorization {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return OAuth2Authorization.posthandle(url, prehandle);
|
return OAuth2AuthorizationController.posthandle(url, prehandle);
|
||||||
};
|
};
|
||||||
|
|
||||||
static actionRequest = async ({
|
static actionRequest = async ({
|
||||||
@ -242,7 +243,7 @@ export class OAuth2Authorization {
|
|||||||
url: URL;
|
url: URL;
|
||||||
request: Request;
|
request: Request;
|
||||||
}) => {
|
}) => {
|
||||||
const prehandle = await OAuth2Authorization.prehandle(url, locals);
|
const prehandle = await OAuth2AuthorizationController.prehandle(url, locals);
|
||||||
const { client, scope, user } = prehandle;
|
const { client, scope, user } = prehandle;
|
||||||
|
|
||||||
const prompt = ((url.searchParams.get('prompt') || '') as string).split(' ');
|
const prompt = ((url.searchParams.get('prompt') || '') as string).split(' ');
|
||||||
@ -262,11 +263,11 @@ export class OAuth2Authorization {
|
|||||||
throw new AccessDenied('User denied access to the resource');
|
throw new AccessDenied('User denied access to the resource');
|
||||||
}
|
}
|
||||||
|
|
||||||
// req.oauth2.logger.debug('Decision check passed');
|
// console.debug('Decision check passed');
|
||||||
|
|
||||||
await OAuth2Users.saveConsent(user, client, scope);
|
await OAuth2Users.saveConsent(user, client, scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
return OAuth2Authorization.posthandle(url, prehandle);
|
return OAuth2AuthorizationController.posthandle(url, prehandle);
|
||||||
};
|
};
|
||||||
}
|
}
|
3
src/lib/server/oauth2/controller/index.ts
Normal file
3
src/lib/server/oauth2/controller/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './authorization';
|
||||||
|
export * from './introspection';
|
||||||
|
export * from './token';
|
58
src/lib/server/oauth2/controller/introspection.ts
Normal file
58
src/lib/server/oauth2/controller/introspection.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { InvalidRequest } from '../error';
|
||||||
|
import { OAuth2AccessTokens } from '../model';
|
||||||
|
import { OAuth2Response } from '../response';
|
||||||
|
|
||||||
|
export class OAuth2IntrospectionController {
|
||||||
|
static postHandler = async ({ request, url }: { request: Request; url: URL }) => {
|
||||||
|
const body = await request.json().catch(() => ({}));
|
||||||
|
|
||||||
|
let clientId: string | null = null;
|
||||||
|
let clientSecret: string | null = null;
|
||||||
|
|
||||||
|
if (body.client_id && body.client_secret) {
|
||||||
|
clientId = body.client_id as string;
|
||||||
|
clientSecret = body.client_secret as string;
|
||||||
|
console.debug('Client credentials parsed from body parameters ', clientId, clientSecret);
|
||||||
|
} else {
|
||||||
|
if (!request.headers?.has('authorization')) {
|
||||||
|
throw new InvalidRequest('No authorization header passed');
|
||||||
|
}
|
||||||
|
|
||||||
|
let pieces = (request.headers.get('authorization') as string).split(' ', 2);
|
||||||
|
if (!pieces || pieces.length !== 2) {
|
||||||
|
throw new InvalidRequest('Authorization header is corrupted');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pieces[0] !== 'Basic') {
|
||||||
|
throw new InvalidRequest(`Unsupported authorization method: ${pieces[0]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
pieces = Buffer.from(pieces[1], 'base64').toString('ascii').split(':', 2);
|
||||||
|
if (!pieces || pieces.length !== 2) {
|
||||||
|
throw new InvalidRequest('Authorization header has corrupted data');
|
||||||
|
}
|
||||||
|
|
||||||
|
clientId = pieces[0];
|
||||||
|
clientSecret = pieces[1];
|
||||||
|
console.debug('Client credentials parsed from basic auth header: ', clientId, clientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.token) {
|
||||||
|
throw new InvalidRequest('Token not provided in request body');
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await OAuth2AccessTokens.fetchByToken(body.token);
|
||||||
|
if (!token) {
|
||||||
|
throw new InvalidRequest('Token does not exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ttl = OAuth2AccessTokens.getTTL(token);
|
||||||
|
const resObj = {
|
||||||
|
token_type: 'bearer',
|
||||||
|
token: token.token,
|
||||||
|
expires_in: Math.floor(ttl / 1000)
|
||||||
|
};
|
||||||
|
|
||||||
|
return OAuth2Response.response(url, resObj);
|
||||||
|
};
|
||||||
|
}
|
97
src/lib/server/oauth2/controller/token.ts
Normal file
97
src/lib/server/oauth2/controller/token.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import {
|
||||||
|
InvalidRequest,
|
||||||
|
InvalidClient,
|
||||||
|
UnauthorizedClient,
|
||||||
|
OAuth2Error,
|
||||||
|
UnsupportedGrantType,
|
||||||
|
ServerError
|
||||||
|
} from '../error';
|
||||||
|
import { OAuth2Clients } from '../model';
|
||||||
|
import { OAuth2Response, type OAuth2TokenResponse } from '../response';
|
||||||
|
import * as tokens from './tokens';
|
||||||
|
|
||||||
|
export class OAuth2TokenController {
|
||||||
|
static postHandler = async ({ url, request }: { url: URL; request: Request }) => {
|
||||||
|
let clientId: string | null = null;
|
||||||
|
let clientSecret: string | null = null;
|
||||||
|
let grantType: string | null = null;
|
||||||
|
|
||||||
|
const body = await request.json().catch(() => ({}));
|
||||||
|
|
||||||
|
if (body.client_id && body.client_secret) {
|
||||||
|
clientId = body.client_id as string;
|
||||||
|
clientSecret = body.client_secret as string;
|
||||||
|
console.debug('Client credentials parsed from body parameters', clientId, clientSecret);
|
||||||
|
} else {
|
||||||
|
if (!request.headers?.has('authorization')) {
|
||||||
|
throw new InvalidRequest('No authorization header passed');
|
||||||
|
}
|
||||||
|
|
||||||
|
let pieces = (request.headers.get('authorization') as string).split(' ', 2);
|
||||||
|
if (!pieces || pieces.length !== 2) {
|
||||||
|
throw new InvalidRequest('Authorization header is corrupted');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pieces[0] !== 'Basic') {
|
||||||
|
throw new InvalidRequest(`Unsupported authorization method: ${pieces[0]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
pieces = Buffer.from(pieces[1], 'base64').toString('ascii').split(':', 2);
|
||||||
|
if (!pieces || pieces.length !== 2) {
|
||||||
|
throw new InvalidRequest('Authorization header has corrupted data');
|
||||||
|
}
|
||||||
|
|
||||||
|
clientId = pieces[0];
|
||||||
|
clientSecret = pieces[1];
|
||||||
|
console.debug('Client credentials parsed from basic auth header:', clientId, clientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.grant_type) {
|
||||||
|
throw new InvalidRequest('Request body does not contain grant_type parameter');
|
||||||
|
}
|
||||||
|
|
||||||
|
grantType = body.grant_type as string;
|
||||||
|
console.debug('Parameter grant_type is', grantType);
|
||||||
|
|
||||||
|
const client = await OAuth2Clients.fetchById(clientId);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
throw new InvalidClient('Client not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const valid = OAuth2Clients.checkSecret(client, clientSecret);
|
||||||
|
if (!valid) {
|
||||||
|
throw new UnauthorizedClient('Invalid client secret');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!OAuth2Clients.checkGrantType(client, grantType) && grantType !== 'refresh_token') {
|
||||||
|
throw new UnauthorizedClient('Invalid grant type for the client');
|
||||||
|
} else {
|
||||||
|
console.debug('Grant type check passed');
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokenResponse: OAuth2TokenResponse = {};
|
||||||
|
try {
|
||||||
|
switch (grantType) {
|
||||||
|
case 'authorization_code':
|
||||||
|
tokenResponse = await tokens.authorizationCode(client, body.code, body.code_verifier);
|
||||||
|
break;
|
||||||
|
case 'client_credentials':
|
||||||
|
tokenResponse = await tokens.clientCredentials(client, body.scope);
|
||||||
|
break;
|
||||||
|
case 'refresh_token':
|
||||||
|
tokenResponse = await tokens.refreshToken(client, body.refresh_token);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new UnsupportedGrantType('Grant type does not match any supported type');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenResponse) {
|
||||||
|
return OAuth2Response.response(url, tokenResponse);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return OAuth2Response.error(url, e as OAuth2Error);
|
||||||
|
}
|
||||||
|
throw new ServerError('Internal error');
|
||||||
|
};
|
||||||
|
}
|
140
src/lib/server/oauth2/controller/tokens/authorizationCode.ts
Normal file
140
src/lib/server/oauth2/controller/tokens/authorizationCode.ts
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import { CryptoUtils } from '$lib/server/crypto-utils';
|
||||||
|
import type { OAuth2Client, User } from '$lib/server/drizzle';
|
||||||
|
import { Users } from '$lib/server/users';
|
||||||
|
import { InvalidRequest, ServerError, InvalidGrant } from '../../error';
|
||||||
|
import {
|
||||||
|
OAuth2AccessTokens,
|
||||||
|
OAuth2Clients,
|
||||||
|
OAuth2Codes,
|
||||||
|
OAuth2RefreshTokens,
|
||||||
|
OAuth2Tokens,
|
||||||
|
type OAuth2Code
|
||||||
|
} from '../../model';
|
||||||
|
import { OAuth2Users } from '../../model/user';
|
||||||
|
import type { OAuth2TokenResponse } from '../../response';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Issue an access token by authorization code
|
||||||
|
* @param oauth2 - OAuth2 instance
|
||||||
|
* @param client - OAuth2 client
|
||||||
|
* @param providedCode - Authorization code
|
||||||
|
* @returns Access token.
|
||||||
|
*/
|
||||||
|
export async function authorizationCode(
|
||||||
|
client: OAuth2Client,
|
||||||
|
providedCode: string,
|
||||||
|
codeVerifier?: string
|
||||||
|
): Promise<OAuth2TokenResponse> {
|
||||||
|
const respObj: OAuth2TokenResponse = {
|
||||||
|
token_type: 'bearer'
|
||||||
|
};
|
||||||
|
|
||||||
|
let code: OAuth2Code | undefined = undefined;
|
||||||
|
|
||||||
|
if (!providedCode) {
|
||||||
|
throw new InvalidRequest('code is mandatory for authorization_code grant type');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
code = await OAuth2Codes.fetchByCode(providedCode);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
throw new ServerError('Failed to call code.fetchByCode function');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
if (code.clientId !== client.id) {
|
||||||
|
throw new InvalidGrant('Code was issued by another client');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!OAuth2Codes.checkTTL(code)) {
|
||||||
|
throw new InvalidGrant('Code has already expired');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new InvalidGrant('Code not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug('Code fetched', code);
|
||||||
|
|
||||||
|
const scope = code.scope || '';
|
||||||
|
const cleanScope = OAuth2Clients.transformScope(scope);
|
||||||
|
const userId = code.userId as number;
|
||||||
|
const clientId = client.client_id;
|
||||||
|
|
||||||
|
if (OAuth2Codes.getCodeChallenge) {
|
||||||
|
const { challenge, method } = OAuth2Codes.getCodeChallenge(code);
|
||||||
|
|
||||||
|
if (challenge && method) {
|
||||||
|
if (!codeVerifier) {
|
||||||
|
throw new InvalidGrant('Code verifier is required!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === 'plain' && !CryptoUtils.safeCompare(challenge, codeVerifier)) {
|
||||||
|
throw new InvalidGrant('Invalid code verifier!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
method === 'S256' &&
|
||||||
|
!CryptoUtils.safeCompare(
|
||||||
|
CryptoUtils.createS256(codeVerifier),
|
||||||
|
CryptoUtils.sanitizeS256(challenge)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new InvalidGrant('Invalid code verifier!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug('Code passed PCKE check');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!OAuth2Clients.checkGrantType(client, 'refresh_token')) {
|
||||||
|
console.debug('Client does not allow grant type refresh_token, skip creation');
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
respObj.refresh_token = await OAuth2RefreshTokens.create(userId, clientId, scope);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
throw new ServerError('Failed to call refreshToken.create function');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
respObj.access_token = await OAuth2AccessTokens.create(
|
||||||
|
userId,
|
||||||
|
clientId,
|
||||||
|
scope,
|
||||||
|
OAuth2Tokens.tokenTtl
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
throw new ServerError('Failed to call accessToken.create function');
|
||||||
|
}
|
||||||
|
|
||||||
|
respObj.expires_in = OAuth2Tokens.tokenTtl;
|
||||||
|
console.debug('Access token saved:', respObj.access_token);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await OAuth2Codes.removeByCode(providedCode);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
throw new ServerError('Failed to call code.removeByCode function');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanScope.includes('openid')) {
|
||||||
|
const user = await Users.getById(userId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
respObj.id_token = await OAuth2Users.issueIdToken(
|
||||||
|
user as User,
|
||||||
|
client,
|
||||||
|
cleanScope,
|
||||||
|
code.nonce || undefined
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
throw new ServerError('Failed to issue an ID token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return respObj;
|
||||||
|
}
|
44
src/lib/server/oauth2/controller/tokens/clientCredentials.ts
Normal file
44
src/lib/server/oauth2/controller/tokens/clientCredentials.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import type { OAuth2Client } from '$lib/server/drizzle';
|
||||||
|
import { InvalidScope, ServerError } from '../../error';
|
||||||
|
import { OAuth2AccessTokens, OAuth2Clients, OAuth2Tokens } from '../../model';
|
||||||
|
import type { OAuth2TokenResponse } from '../../response';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Issue client access token
|
||||||
|
* @param oauth2 - OAuth2 instance
|
||||||
|
* @param client - Client
|
||||||
|
* @param wantScope - Requested scopes
|
||||||
|
* @returns Access token
|
||||||
|
*/
|
||||||
|
export async function clientCredentials(
|
||||||
|
client: OAuth2Client,
|
||||||
|
wantScope: string | string[]
|
||||||
|
): Promise<OAuth2TokenResponse> {
|
||||||
|
let scope: string[] = [];
|
||||||
|
|
||||||
|
const resObj: OAuth2TokenResponse = {
|
||||||
|
token_type: 'bearer'
|
||||||
|
};
|
||||||
|
|
||||||
|
scope = OAuth2Clients.transformScope(wantScope);
|
||||||
|
if (!OAuth2Clients.checkScope(client, scope)) {
|
||||||
|
throw new InvalidScope('Client does not allow access to this scope');
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.debug('Scope check passed', scope);
|
||||||
|
|
||||||
|
try {
|
||||||
|
resObj.access_token = await OAuth2AccessTokens.create(
|
||||||
|
null,
|
||||||
|
client.client_id,
|
||||||
|
scope,
|
||||||
|
OAuth2Tokens.tokenTtl
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
throw new ServerError('Failed to call accessToken.create function');
|
||||||
|
}
|
||||||
|
|
||||||
|
resObj.expires_in = OAuth2Tokens.tokenTtl;
|
||||||
|
|
||||||
|
return resObj;
|
||||||
|
}
|
3
src/lib/server/oauth2/controller/tokens/index.ts
Normal file
3
src/lib/server/oauth2/controller/tokens/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './authorizationCode';
|
||||||
|
export * from './clientCredentials';
|
||||||
|
export * from './refreshToken';
|
99
src/lib/server/oauth2/controller/tokens/refreshToken.ts
Normal file
99
src/lib/server/oauth2/controller/tokens/refreshToken.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import type { OAuth2Client, User } from '$lib/server/drizzle';
|
||||||
|
import { Users } from '$lib/server/users';
|
||||||
|
import { InvalidRequest, ServerError, InvalidGrant, InvalidClient } from '../../error';
|
||||||
|
import {
|
||||||
|
type OAuth2RefreshToken,
|
||||||
|
type OAuth2AccessToken,
|
||||||
|
OAuth2RefreshTokens,
|
||||||
|
OAuth2AccessTokens,
|
||||||
|
OAuth2Tokens
|
||||||
|
} from '../../model';
|
||||||
|
import type { OAuth2TokenResponse } from '../../response';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a new access token from a refresh token. Scope change may not be requested.
|
||||||
|
* @param oauth2 - OAuth2 instance
|
||||||
|
* @param client - OAuth2 client
|
||||||
|
* @param providedToken - Refresh token
|
||||||
|
* @returns Access token
|
||||||
|
*/
|
||||||
|
export async function refreshToken(
|
||||||
|
client: OAuth2Client,
|
||||||
|
providedToken: string
|
||||||
|
): Promise<OAuth2TokenResponse> {
|
||||||
|
let user: User | undefined = undefined;
|
||||||
|
let ttl: number | null = null;
|
||||||
|
let refreshToken: OAuth2RefreshToken | undefined = undefined;
|
||||||
|
let accessToken: OAuth2AccessToken | undefined = undefined;
|
||||||
|
|
||||||
|
const resObj: OAuth2TokenResponse = {
|
||||||
|
token_type: 'bearer'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!providedToken) {
|
||||||
|
throw new InvalidRequest('refresh_token is mandatory for refresh_token grant type');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
refreshToken = await OAuth2RefreshTokens.fetchByToken(providedToken);
|
||||||
|
} catch (err) {
|
||||||
|
throw new ServerError('Failed to call refreshToken.fetchByToken function');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!refreshToken) {
|
||||||
|
throw new InvalidGrant('Refresh token not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refreshToken.clientId !== client.id) {
|
||||||
|
console.warn(
|
||||||
|
'Client %s tried to fetch a refresh token which belongs to client %s!',
|
||||||
|
client.id,
|
||||||
|
refreshToken.clientId
|
||||||
|
);
|
||||||
|
throw new InvalidGrant('Refresh token not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
user = await Users.getById(refreshToken.userId as number);
|
||||||
|
} catch (err) {
|
||||||
|
throw new InvalidClient('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new InvalidClient('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
accessToken = await OAuth2AccessTokens.fetchByUserIdClientId(user.id, client.client_id);
|
||||||
|
} catch (err) {
|
||||||
|
throw new ServerError('Failed to call accessToken.fetchByUserIdClientId function');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessToken) {
|
||||||
|
ttl = OAuth2AccessTokens.getTTL(accessToken);
|
||||||
|
|
||||||
|
if (!ttl) {
|
||||||
|
accessToken = undefined;
|
||||||
|
} else {
|
||||||
|
resObj.access_token = accessToken.token;
|
||||||
|
resObj.expires_in = ttl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
try {
|
||||||
|
resObj.access_token = await OAuth2AccessTokens.create(
|
||||||
|
user.id,
|
||||||
|
client.client_id,
|
||||||
|
refreshToken.scope || '',
|
||||||
|
OAuth2Tokens.tokenTtl
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
throw new ServerError('Failed to call accessToken.create function');
|
||||||
|
}
|
||||||
|
|
||||||
|
resObj.expires_in = OAuth2Tokens.tokenTtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resObj;
|
||||||
|
}
|
@ -1 +1,4 @@
|
|||||||
export class OAuth2 {}
|
export * from './controller';
|
||||||
|
export * from './model';
|
||||||
|
export * from './error';
|
||||||
|
export * from './response';
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export * from './client';
|
export * from './client';
|
||||||
export * from './tokens';
|
export * from './tokens';
|
||||||
|
export * from './user';
|
||||||
|
@ -10,6 +10,7 @@ import { and, eq, sql } from 'drizzle-orm';
|
|||||||
import { OAuth2Clients } from './client';
|
import { OAuth2Clients } from './client';
|
||||||
import { Users } from '$lib/server/users';
|
import { Users } from '$lib/server/users';
|
||||||
import { CryptoUtils } from '$lib/server/crypto-utils';
|
import { CryptoUtils } from '$lib/server/crypto-utils';
|
||||||
|
import { AccessDenied } from '../error';
|
||||||
|
|
||||||
export type CodeChallengeMethod = 'plain' | 'S256';
|
export type CodeChallengeMethod = 'plain' | 'S256';
|
||||||
|
|
||||||
@ -174,11 +175,11 @@ export class OAuth2Codes {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkTTL(code: OAuth2Code): boolean {
|
static checkTTL(code: OAuth2Code): boolean {
|
||||||
return new Date(code.expires_at).getTime() > Date.now();
|
return new Date(code.expires_at).getTime() > Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
getCodeChallenge(code: OAuth2Code) {
|
static getCodeChallenge(code: OAuth2Code) {
|
||||||
return {
|
return {
|
||||||
method: code.code_challenge_method,
|
method: code.code_challenge_method,
|
||||||
challenge: code.code_challenge
|
challenge: code.code_challenge
|
||||||
@ -187,9 +188,14 @@ export class OAuth2Codes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class OAuth2AccessTokens {
|
export class OAuth2AccessTokens {
|
||||||
static async create(userId: number, clientId: string, scope: string | string[], ttl: number) {
|
static async create(
|
||||||
|
userId: number | null,
|
||||||
|
clientId: string,
|
||||||
|
scope: string | string[],
|
||||||
|
ttl: number
|
||||||
|
) {
|
||||||
const client = await OAuth2Clients.fetchById(clientId);
|
const client = await OAuth2Clients.fetchById(clientId);
|
||||||
const user = await Users.getById(userId);
|
const user = userId != null ? await Users.getById(userId) : undefined;
|
||||||
const accessToken = CryptoUtils.generateString(64);
|
const accessToken = CryptoUtils.generateString(64);
|
||||||
|
|
||||||
const scopes = (!Array.isArray(scope) ? OAuth2Clients.splitScope(scope) : scope).join(' ');
|
const scopes = (!Array.isArray(scope) ? OAuth2Clients.splitScope(scope) : scope).join(' ');
|
||||||
@ -248,6 +254,46 @@ export class OAuth2AccessTokens {
|
|||||||
clientIdPub: find.o_auth2_client.client_id
|
clientIdPub: find.o_auth2_client.client_id
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getFromRequest(request: Request, url?: URL): Promise<OAuth2AccessToken | undefined> {
|
||||||
|
let token = null;
|
||||||
|
|
||||||
|
// Look for token in header
|
||||||
|
if (request.headers.has('authorization')) {
|
||||||
|
const pieces = (request.headers.get('authorization') as string).split(' ', 2);
|
||||||
|
|
||||||
|
// Check authorization header
|
||||||
|
if (!pieces || pieces.length !== 2) {
|
||||||
|
throw new AccessDenied('Wrong authorization header');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only bearer auth is supported
|
||||||
|
if (pieces[0].toLowerCase() !== 'bearer') {
|
||||||
|
throw new AccessDenied('Unsupported authorization method in header');
|
||||||
|
}
|
||||||
|
|
||||||
|
token = pieces[1];
|
||||||
|
} else if (request.headers.has('x-access-token')) {
|
||||||
|
token = request.headers.get('x-access-token');
|
||||||
|
} else if (url?.searchParams.has('access_token')) {
|
||||||
|
token = url.searchParams.get('access_token') as string;
|
||||||
|
} else {
|
||||||
|
const body = await request.json().catch(() => ({}));
|
||||||
|
if (!body.access_token) {
|
||||||
|
throw new AccessDenied('Bearer token not found');
|
||||||
|
}
|
||||||
|
token = body.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to fetch access token
|
||||||
|
const object = await OAuth2AccessTokens.fetchByToken(token);
|
||||||
|
if (!object) {
|
||||||
|
throw new AccessDenied('Token not found or has expired');
|
||||||
|
} else if (!OAuth2AccessTokens.checkTTL(object)) {
|
||||||
|
throw new AccessDenied('Token is expired');
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OAuth2RefreshTokens {
|
export class OAuth2RefreshTokens {
|
||||||
@ -286,7 +332,7 @@ export class OAuth2RefreshTokens {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeByRefreshToken(token: string): Promise<boolean> {
|
static async removeByRefreshToken(token: string): Promise<boolean> {
|
||||||
const find = await OAuth2Tokens.fetchByToken(token, OAuth2TokenType.REFRESH_TOKEN);
|
const find = await OAuth2Tokens.fetchByToken(token, OAuth2TokenType.REFRESH_TOKEN);
|
||||||
|
|
||||||
await OAuth2Tokens.remove(find);
|
await OAuth2Tokens.remove(find);
|
||||||
|
@ -29,6 +29,13 @@ export class OAuth2Response {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static createErrorResponse(err: OAuth2Error) {
|
||||||
|
return OAuth2Response.createResponse(err.status, {
|
||||||
|
error: err.code,
|
||||||
|
error_description: err.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static redirect(redirectUri: string) {
|
static redirect(redirectUri: string) {
|
||||||
return redirect(302, redirectUri);
|
return redirect(302, redirectUri);
|
||||||
}
|
}
|
||||||
@ -53,10 +60,7 @@ export class OAuth2Response {
|
|||||||
return redirect(302, redirectUri);
|
return redirect(302, redirectUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
return OAuth2Response.createResponse(err.status, {
|
return OAuth2Response.createErrorResponse(err);
|
||||||
error: err.code,
|
|
||||||
error_description: err.message
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static errorPlain(url: URL, err: OAuth2Error, redirectUri?: string) {
|
static errorPlain(url: URL, err: OAuth2Error, redirectUri?: string) {
|
||||||
|
64
src/routes/api/user/+server.ts
Normal file
64
src/routes/api/user/+server.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { PUBLIC_URL } from '$env/static/public';
|
||||||
|
import { ApiUtils } from '$lib/server/api-utils.js';
|
||||||
|
import type { User } from '$lib/server/drizzle/schema.js';
|
||||||
|
import { AccessDenied, OAuth2Error } from '$lib/server/oauth2/error.js';
|
||||||
|
import { OAuth2Clients } from '$lib/server/oauth2/model/client.js';
|
||||||
|
import { OAuth2AccessTokens } from '$lib/server/oauth2/model/tokens.js';
|
||||||
|
import { OAuth2Response } from '$lib/server/oauth2/response.js';
|
||||||
|
import { Users } from '$lib/server/users/index.js';
|
||||||
|
|
||||||
|
export const GET = async ({ request, url, locals }) => {
|
||||||
|
let user: User | undefined = undefined;
|
||||||
|
let tokenScopes: string[] | undefined = undefined;
|
||||||
|
|
||||||
|
if (locals.session?.data?.user) {
|
||||||
|
// From session
|
||||||
|
user = await Users.getBySession(locals.session.data.user);
|
||||||
|
} else {
|
||||||
|
// From OAuth2 bearer token
|
||||||
|
try {
|
||||||
|
const token = await OAuth2AccessTokens.getFromRequest(request, url);
|
||||||
|
if (token?.userId) {
|
||||||
|
tokenScopes = OAuth2Clients.splitScope(token.scope || '');
|
||||||
|
user = await Users.getById(token.userId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof OAuth2Error) {
|
||||||
|
return OAuth2Response.createErrorResponse(error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return OAuth2Response.createErrorResponse(new AccessDenied('Access denied'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopelessAccess = !tokenScopes || tokenScopes.includes('management');
|
||||||
|
|
||||||
|
const userData: Record<string, unknown> = {
|
||||||
|
id: user.id,
|
||||||
|
uuid: user.uuid,
|
||||||
|
username: user.username,
|
||||||
|
display_name: user.display_name,
|
||||||
|
|
||||||
|
// Standard claims
|
||||||
|
sub: user.uuid,
|
||||||
|
name: user.display_name,
|
||||||
|
preferred_username: user.username,
|
||||||
|
nickname: user.display_name
|
||||||
|
};
|
||||||
|
|
||||||
|
if (scopelessAccess || tokenScopes?.includes('email')) {
|
||||||
|
userData.email = user.email;
|
||||||
|
userData.email_verified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((scopelessAccess || tokenScopes?.includes('picture')) && user.pictureId) {
|
||||||
|
userData.picture = `${PUBLIC_URL}/api/avatar/${user.uuid}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: privileges
|
||||||
|
|
||||||
|
return ApiUtils.json(userData);
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import { OAuth2Authorization } from '$lib/server/oauth2/authorization.js';
|
import { OAuth2AuthorizationController } from '$lib/server/oauth2/controller/authorization.js';
|
||||||
import { OAuth2Error } from '$lib/server/oauth2/error.js';
|
import { OAuth2Error } from '$lib/server/oauth2/error.js';
|
||||||
import { OAuth2Response } from '$lib/server/oauth2/response.js';
|
import { OAuth2Response } from '$lib/server/oauth2/response.js';
|
||||||
import { Users } from '$lib/server/users';
|
import { Users } from '$lib/server/users';
|
||||||
@ -9,7 +9,7 @@ export const actions = {
|
|||||||
locals.user = await Users.readSessionOrRedirect(locals, url);
|
locals.user = await Users.readSessionOrRedirect(locals, url);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await OAuth2Authorization.actionRequest({ request, locals, url });
|
return await OAuth2AuthorizationController.actionRequest({ request, locals, url });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof OAuth2Error) {
|
if (err instanceof OAuth2Error) {
|
||||||
const obj = OAuth2Response.errorPlain(
|
const obj = OAuth2Response.errorPlain(
|
||||||
@ -29,7 +29,7 @@ export const load = async ({ locals, url }) => {
|
|||||||
locals.user = await Users.readSessionOrRedirect(locals, url);
|
locals.user = await Users.readSessionOrRedirect(locals, url);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await OAuth2Authorization.getRequest({ locals, url });
|
return await OAuth2AuthorizationController.getRequest({ locals, url });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof OAuth2Error) {
|
if (err instanceof OAuth2Error) {
|
||||||
return OAuth2Response.errorPlain(url, err, url.searchParams.get('redirect_uri') || undefined);
|
return OAuth2Response.errorPlain(url, err, url.searchParams.get('redirect_uri') || undefined);
|
||||||
|
14
src/routes/oauth2/introspect/+server.ts
Normal file
14
src/routes/oauth2/introspect/+server.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { OAuth2Error } from '$lib/server/oauth2/error.js';
|
||||||
|
import { OAuth2IntrospectionController } from '$lib/server/oauth2/controller/introspection.js';
|
||||||
|
import { OAuth2Response } from '$lib/server/oauth2/response.js';
|
||||||
|
|
||||||
|
export const POST = async ({ request, url }) => {
|
||||||
|
try {
|
||||||
|
return await OAuth2IntrospectionController.postHandler({ request, url });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof OAuth2Error) {
|
||||||
|
return OAuth2Response.error(url, error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
14
src/routes/oauth2/token/+server.ts
Normal file
14
src/routes/oauth2/token/+server.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { OAuth2Error } from '$lib/server/oauth2/error.js';
|
||||||
|
import { OAuth2Response } from '$lib/server/oauth2/response.js';
|
||||||
|
import { OAuth2TokenController } from '$lib/server/oauth2/controller/token.js';
|
||||||
|
|
||||||
|
export const POST = async ({ request, url }) => {
|
||||||
|
try {
|
||||||
|
return await OAuth2TokenController.postHandler({ request, url });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof OAuth2Error) {
|
||||||
|
return OAuth2Response.error(url, error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user