diff --git a/src/lib/components/avatar/AvatarCard.svelte b/src/lib/components/avatar/AvatarCard.svelte index ee00d7b..8376de1 100644 --- a/src/lib/components/avatar/AvatarCard.svelte +++ b/src/lib/components/avatar/AvatarCard.svelte @@ -1,13 +1,11 @@
- {user.username} +
diff --git a/src/lib/i18n/en/oauth2.json b/src/lib/i18n/en/oauth2.json index 85e6955..fec6a4f 100644 --- a/src/lib/i18n/en/oauth2.json +++ b/src/lib/i18n/en/oauth2.json @@ -2,15 +2,22 @@ "authorize": { "title": "Authorize application", "errorPage": "The authorization URL provided is invalid or malformed. Please forward this message to the developers of the application you came here from:", - "authorize": "Authorize", + "authorize": "Authorize the application", "reject": "Reject", + "allowed": "This application will have access to..", + "disallowed": "This application will NOT have access to..", "scope": { - "profile": "Username and display name", - "email": "Email address", - "picture": "Profile picture", - "account": "Password and other account settings", + "profile": "Your username and display name", + "email": "Your email address", + "picture": "Your profile picture", + "account": "Changing your password or other account settings", "management": "Manage Icy Network on your behalf", "admin": "Commit administrative actions to the extent of your user privileges" + }, + "link": { + "website": "Website", + "privacy": "Privacy policy", + "terms": "Terms of Service" } } } diff --git a/src/lib/server/oauth2/controller/bearer.ts b/src/lib/server/oauth2/controller/bearer.ts new file mode 100644 index 0000000..4a243ae --- /dev/null +++ b/src/lib/server/oauth2/controller/bearer.ts @@ -0,0 +1,47 @@ +import { AccessDenied } from '../error'; +import { OAuth2AccessTokens, type OAuth2AccessToken } from '../model'; + +export class OAuth2BearerController { + static bearerFromRequest = async ( + request: Request, + url: URL + ): Promise => { + 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; + }; +} diff --git a/src/lib/server/oauth2/controller/index.ts b/src/lib/server/oauth2/controller/index.ts index 4a69554..fb80845 100644 --- a/src/lib/server/oauth2/controller/index.ts +++ b/src/lib/server/oauth2/controller/index.ts @@ -1,3 +1,4 @@ export * from './authorization'; export * from './introspection'; export * from './token'; +export * from './bearer'; diff --git a/src/lib/server/oauth2/controller/tokens/authorizationCode.ts b/src/lib/server/oauth2/controller/tokens/authorizationCode.ts index 3d3cfe8..703d5f5 100644 --- a/src/lib/server/oauth2/controller/tokens/authorizationCode.ts +++ b/src/lib/server/oauth2/controller/tokens/authorizationCode.ts @@ -15,9 +15,9 @@ import type { OAuth2TokenResponse } from '../../response'; /** * Issue an access token by authorization code - * @param oauth2 - OAuth2 instance * @param client - OAuth2 client * @param providedCode - Authorization code + * @param codeVerifier - PCKE check verifier * @returns Access token. */ export async function authorizationCode( diff --git a/src/lib/server/oauth2/controller/tokens/clientCredentials.ts b/src/lib/server/oauth2/controller/tokens/clientCredentials.ts index fc2003e..67b27c3 100644 --- a/src/lib/server/oauth2/controller/tokens/clientCredentials.ts +++ b/src/lib/server/oauth2/controller/tokens/clientCredentials.ts @@ -5,7 +5,6 @@ import type { OAuth2TokenResponse } from '../../response'; /** * Issue client access token - * @param oauth2 - OAuth2 instance * @param client - Client * @param wantScope - Requested scopes * @returns Access token diff --git a/src/lib/server/oauth2/controller/tokens/refreshToken.ts b/src/lib/server/oauth2/controller/tokens/refreshToken.ts index 3b89955..73e7913 100644 --- a/src/lib/server/oauth2/controller/tokens/refreshToken.ts +++ b/src/lib/server/oauth2/controller/tokens/refreshToken.ts @@ -12,7 +12,6 @@ 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 diff --git a/src/lib/server/oauth2/model/tokens.ts b/src/lib/server/oauth2/model/tokens.ts index 29320a1..0cc2d63 100644 --- a/src/lib/server/oauth2/model/tokens.ts +++ b/src/lib/server/oauth2/model/tokens.ts @@ -10,7 +10,6 @@ import { and, eq, sql } from 'drizzle-orm'; import { OAuth2Clients } from './client'; import { Users } from '$lib/server/users'; import { CryptoUtils } from '$lib/server/crypto-utils'; -import { AccessDenied } from '../error'; export type CodeChallengeMethod = 'plain' | 'S256'; @@ -254,46 +253,6 @@ export class OAuth2AccessTokens { clientIdPub: find.o_auth2_client.client_id }; } - - static async getFromRequest(request: Request, url?: URL): Promise { - 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 { diff --git a/src/routes/account/+page.svelte b/src/routes/account/+page.svelte index 9f9e33b..c85de51 100644 --- a/src/routes/account/+page.svelte +++ b/src/routes/account/+page.svelte @@ -155,7 +155,7 @@

{$t('account.avatar.title')}

- + { } else { // From OAuth2 bearer token try { - const token = await OAuth2AccessTokens.getFromRequest(request, url); + const token = await OAuth2BearerController.bearerFromRequest(request, url); if (token?.userId) { tokenScopes = OAuth2Clients.splitScope(token.scope || ''); user = await Users.getById(token.userId); diff --git a/src/routes/oauth2/authorize/+page.svelte b/src/routes/oauth2/authorize/+page.svelte index e4f11af..b69dce5 100644 --- a/src/routes/oauth2/authorize/+page.svelte +++ b/src/routes/oauth2/authorize/+page.svelte @@ -1,5 +1,5 @@ -

{$t('common.siteName')}

- {#if data.error} +

{$t('common.siteName')}

{$t('oauth2.authorize.errorPage')}

{$t('oauth2.authorize.title')} - {#if data.user} - - - {data.user.name} - @{data.user.username} - - - {/if} - {data.client.title} - {data.client.description} +

{$t('common.siteName')}

+

{$t('oauth2.authorize.title')}

-
- {#each data.client.allowedScopes as scope} - {$t(`oauth2.authorize.scope.${scope}`)} - {/each} +
+ {#if data.user} +
+ +
+ {data.user.name} + @{data.user.username} +
+
+
+ {/if} + +
+ +
+ {data.client.title} + {data.client.description} + + +
+
+
-
- {#each data.client.disallowedScopes as scope} - {$t(`oauth2.authorize.scope.${scope}`)} - {/each} +
+ {#if data.client.allowedScopes?.length} +

{$t('oauth2.authorize.allowed')}

+
+ {#each data.client.allowedScopes as scope} + {$t(`oauth2.authorize.scope.${scope}`)} + {/each} +
+ {/if} + + {#if data.client.disallowedScopes?.length} +

{$t('oauth2.authorize.disallowed')}

+
+ {#each data.client.disallowedScopes as scope} + {$t(`oauth2.authorize.scope.${scope}`)} + {/each} +
+ {/if}
-
- - -
+
+
+ + +
-
- - -
+
+ + +
+
{/if} + +