From 2321a05194591736c6ccb6650bdb646ab1496412 Mon Sep 17 00:00:00 2001 From: barbapapazes Date: Sat, 17 Aug 2024 12:06:48 +0200 Subject: [PATCH 1/6] feat: add types --- playground/server/routes/auth/github.get.ts | 2 +- src/runtime/server/lib/oauth/github.ts | 85 ++++++++++++++++----- src/runtime/types/index.ts | 1 + src/runtime/types/oauth-config.ts | 6 +- src/runtime/types/oauth.ts | 46 +++++++++++ 5 files changed, 115 insertions(+), 25 deletions(-) create mode 100644 src/runtime/types/oauth.ts diff --git a/playground/server/routes/auth/github.get.ts b/playground/server/routes/auth/github.get.ts index fd422ee2..c6a7ff74 100644 --- a/playground/server/routes/auth/github.get.ts +++ b/playground/server/routes/auth/github.get.ts @@ -2,7 +2,7 @@ export default oauthGitHubEventHandler({ async onSuccess(event, { user }) { await setUserSession(event, { user: { - github: user.login, + github: user.nickname, }, loggedInAt: Date.now(), }) diff --git a/src/runtime/server/lib/oauth/github.ts b/src/runtime/server/lib/oauth/github.ts index e949e672..b5be08d6 100644 --- a/src/runtime/server/lib/oauth/github.ts +++ b/src/runtime/server/lib/oauth/github.ts @@ -1,9 +1,60 @@ -import type { H3Event } from 'h3' +import type { H3Event, EventHandler } from 'h3' import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from 'h3' import { withQuery } from 'ufo' import { defu } from 'defu' import { useRuntimeConfig } from '#imports' -import type { OAuthConfig } from '#auth-utils' +import type { OAuthConfig, OAuthTokens, OAuthUser } from '#auth-utils' + +type GitHubError = { + error: string + error_description: string + error_uri: string +} + +type GitHubTokens = { + access_token: string + token_type: string + scope: string +} + +type GitHubUser = { + login: string + id: number + node_id: string + avatar_url: string + name: string + email: string +} + +/** + * @see https://docs.github.com/en/rest/users/emails?apiVersion=2022-11-28#list-email-addresses-for-the-authenticated-user + */ +type GitHubEmail = { + email: string + primary: boolean + verified: boolean + visibility: string | null +} + +function normalizeGitHubUser(user: GitHubUser): OAuthUser { + return { + id: user.id, + nickname: user.login, + name: user.name, + email: user.email, + avatar: user.avatar_url, + raw: user, + } +} + +function normalizeGitHubTokens(tokens: GitHubTokens): OAuthTokens { + return { + token: tokens.access_token, + refreshToken: '', + expiresIn: 0, + approvedScopes: tokens.scope.split(','), + } +} export interface OAuthGitHubConfig { /** @@ -56,7 +107,7 @@ export interface OAuthGitHubConfig { redirectURL?: string } -export function oauthGitHubEventHandler({ config, onSuccess, onError }: OAuthConfig) { +export function oauthGitHubEventHandler({ config, onSuccess, onError }: OAuthConfig): EventHandler { return eventHandler(async (event: H3Event) => { config = defu(config, useRuntimeConfig(event).oauth?.github, { authorizationURL: 'https://github.com/login/oauth/authorize', @@ -102,9 +153,7 @@ export function oauthGitHubEventHandler({ config, onSuccess, onError }: OAuthCon ) } - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch( + const tokens = await $fetch( config.tokenURL as string, { method: 'POST', @@ -115,20 +164,18 @@ export function oauthGitHubEventHandler({ config, onSuccess, onError }: OAuthCon }, }, ) - if (tokens.error) { + if ((tokens as GitHubError).error) { const error = createError({ statusCode: 401, - message: `GitHub login failed: ${tokens.error || 'Unknown error'}`, - data: tokens, + message: `GitHub login failed: ${(tokens as GitHubError).error || 'Unknown error'}`, + data: tokens as GitHubError, }) if (!onError) throw error return onError(event, error) } - const accessToken = tokens.access_token - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const user: any = await $fetch('https://api.github.com/user', { + const accessToken = (tokens as GitHubTokens).access_token + const user: GitHubUser = await $fetch('https://api.github.com/user', { headers: { 'User-Agent': `Github-OAuth-${config.clientId}`, 'Authorization': `token ${accessToken}`, @@ -137,17 +184,13 @@ export function oauthGitHubEventHandler({ config, onSuccess, onError }: OAuthCon // if no public email, check the private ones if (!user.email && config.emailRequired) { - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const emails: any[] = await $fetch('https://api.github.com/user/emails', { + const emails: GitHubEmail[] = await $fetch('https://api.github.com/user/emails', { headers: { 'User-Agent': `Github-OAuth-${config.clientId}`, 'Authorization': `token ${accessToken}`, }, }) - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const primaryEmail = emails.find((email: any) => email.primary) + const primaryEmail = emails.find(email => email.primary) // Still no email if (!primaryEmail) { throw new Error('GitHub login failed: no user email found') @@ -156,8 +199,8 @@ export function oauthGitHubEventHandler({ config, onSuccess, onError }: OAuthCon } return onSuccess(event, { - user, - tokens, + user: normalizeGitHubUser(user), + tokens: normalizeGitHubTokens(tokens as GitHubTokens), }) }) } diff --git a/src/runtime/types/index.ts b/src/runtime/types/index.ts index 2a759eb9..5dbbc0d8 100644 --- a/src/runtime/types/index.ts +++ b/src/runtime/types/index.ts @@ -1,2 +1,3 @@ export type { User, UserSession, UserSessionRequired, UserSessionComposable } from './session' export type { OAuthConfig } from './oauth-config' +export type { OAuthTokens, OAuthUser } from './oauth' diff --git a/src/runtime/types/oauth-config.ts b/src/runtime/types/oauth-config.ts index c4c07c9d..36d18611 100644 --- a/src/runtime/types/oauth-config.ts +++ b/src/runtime/types/oauth-config.ts @@ -1,11 +1,11 @@ import type { H3Event, H3Error } from 'h3' +import type { OAuthTokens, OAuthUser } from '#auth-utils' -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export interface OAuthConfig { +export interface OAuthConfig> { config?: TConfig onSuccess: ( event: H3Event, - result: { user: TUser, tokens: TTokens } + result: { user: OAuthUser, tokens: OAuthTokens } ) => Promise | void onError?: (event: H3Event, error: H3Error) => Promise | void } diff --git a/src/runtime/types/oauth.ts b/src/runtime/types/oauth.ts new file mode 100644 index 00000000..c3f31497 --- /dev/null +++ b/src/runtime/types/oauth.ts @@ -0,0 +1,46 @@ +export interface OAuthUser> { + /** + * Unique identifier for the user + */ + id: string | number + /** + * The user's username + */ + nickname: string + /** + * The user's full name + */ + name: string + /** + * The user's email address + */ + email: string + /** + * The user's profile picture URL + */ + avatar: string + + /** + * The raw user object from the provider + */ + raw: RawUser +} + +export interface OAuthTokens { + /** + * The access token to use for API requests + */ + token: string + /** + * The refresh token to use to get a new access token + */ + refreshToken: string + /** + * The token type + */ + expiresIn: number + /** + * The scope of the access token + */ + approvedScopes: string[] +} From 7305ef187284fb1941ea969d9f10a594f2c823cf Mon Sep 17 00:00:00 2001 From: barbapapazes Date: Sat, 17 Aug 2024 15:12:24 +0200 Subject: [PATCH 2/6] feat: improve oauth types and migrate discord --- playground/server/routes/auth/discord.get.ts | 2 +- src/runtime/server/lib/oauth/discord.ts | 64 ++++++++++++++------ src/runtime/server/lib/oauth/github.ts | 36 +++++------ src/runtime/types/index.ts | 2 +- src/runtime/types/oauth-config.ts | 4 +- src/runtime/types/oauth.ts | 40 +++++++++--- 6 files changed, 95 insertions(+), 53 deletions(-) diff --git a/playground/server/routes/auth/discord.get.ts b/playground/server/routes/auth/discord.get.ts index 12fbe4be..fd9436a8 100644 --- a/playground/server/routes/auth/discord.get.ts +++ b/playground/server/routes/auth/discord.get.ts @@ -2,7 +2,7 @@ export default oauthDiscordEventHandler({ async onSuccess(event, { user }) { await setUserSession(event, { user: { - discord: user.username, + discord: user.nickname, }, loggedInAt: Date.now(), }) diff --git a/src/runtime/server/lib/oauth/discord.ts b/src/runtime/server/lib/oauth/discord.ts index 27baaee3..90b84d9a 100644 --- a/src/runtime/server/lib/oauth/discord.ts +++ b/src/runtime/server/lib/oauth/discord.ts @@ -1,9 +1,43 @@ -import type { H3Event } from 'h3' +import type { H3Event, EventHandler } from 'h3' import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from 'h3' import { withQuery, parseURL, stringifyParsedURL } from 'ufo' import { defu } from 'defu' import { useRuntimeConfig } from '#imports' -import type { OAuthConfig } from '#auth-utils' +import type { OAuthAccessTokenError, OAuthAccessTokenSuccess, OAuthConfig, OAuthToken } from '#auth-utils' + +/** + * @see https://discord.com/developers/docs/resources/user#user-object + */ +type DiscordUser = { + id: string + discriminator: string + username: string + avatar: string + email?: string +} + +function normalizeDiscordUser(user: DiscordUser) { + return { + id: user.id, + nickname: user.username, + avatar: user.avatar + ? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.${ + user.avatar.startsWith('a_') ? 'gif' : 'png' + }` + : `https://cdn.discordapp.com/embed/avatars/${user.discriminator === '0' ? (Number(user.id) >> 22) % 6 : Number(user.discriminator) % 5}.png`, + email: user.email, + raw: user, + } +} + +function normalizeDiscordTokens(tokens: OAuthAccessTokenSuccess): OAuthToken { + return { + token: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresIn: tokens.expires_in, + approvedScopes: tokens.scope?.split(' '), + } +} export interface OAuthDiscordConfig { /** @@ -58,7 +92,7 @@ export interface OAuthDiscordConfig { redirectURL?: string } -export function oauthDiscordEventHandler({ config, onSuccess, onError }: OAuthConfig) { +export function oauthDiscordEventHandler({ config, onSuccess, onError }: OAuthConfig): EventHandler { return eventHandler(async (event: H3Event) => { config = defu(config, useRuntimeConfig(event).oauth?.discord, { authorizationURL: 'https://discord.com/oauth2/authorize', @@ -102,9 +136,7 @@ export function oauthDiscordEventHandler({ config, onSuccess, onError }: OAuthCo const parsedRedirectUrl = parseURL(redirectURL) parsedRedirectUrl.search = '' - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch( + const tokens = await $fetch( config.tokenURL as string, { method: 'POST', @@ -119,24 +151,20 @@ export function oauthDiscordEventHandler({ config, onSuccess, onError }: OAuthCo code: code as string, }).toString(), }, - ).catch((error) => { - return { error } - }) - if (tokens.error) { + ) + if ((tokens as OAuthAccessTokenError).error) { const error = createError({ statusCode: 401, - message: `Discord login failed: ${tokens.error?.data?.error_description || 'Unknown error'}`, - data: tokens, + message: `Discord login failed: ${(tokens as OAuthAccessTokenError).error || 'Unknown error'}`, + data: tokens as OAuthAccessTokenError, }) if (!onError) throw error return onError(event, error) } - const accessToken = tokens.access_token - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const user: any = await $fetch('https://discord.com/api/users/@me', { + const accessToken = (tokens as OAuthAccessTokenSuccess).access_token + const user: DiscordUser = await $fetch('https://discord.com/api/users/@me', { headers: { 'user-agent': 'Nuxt Auth Utils', 'Authorization': `Bearer ${accessToken}`, @@ -144,8 +172,8 @@ export function oauthDiscordEventHandler({ config, onSuccess, onError }: OAuthCo }) return onSuccess(event, { - tokens, - user, + user: normalizeDiscordUser(user), + tokens: normalizeDiscordTokens(tokens as OAuthAccessTokenSuccess), }) }) } diff --git a/src/runtime/server/lib/oauth/github.ts b/src/runtime/server/lib/oauth/github.ts index b5be08d6..b6fe77e7 100644 --- a/src/runtime/server/lib/oauth/github.ts +++ b/src/runtime/server/lib/oauth/github.ts @@ -3,20 +3,11 @@ import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from import { withQuery } from 'ufo' import { defu } from 'defu' import { useRuntimeConfig } from '#imports' -import type { OAuthConfig, OAuthTokens, OAuthUser } from '#auth-utils' - -type GitHubError = { - error: string - error_description: string - error_uri: string -} - -type GitHubTokens = { - access_token: string - token_type: string - scope: string -} +import type { OAuthConfig, OAuthToken, OAuthUser, OAuthAccessTokenSuccess, OAuthAccessTokenError } from '#auth-utils' +/** + * @see https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user + */ type GitHubUser = { login: string id: number @@ -24,6 +15,8 @@ type GitHubUser = { avatar_url: string name: string email: string + + [key: string]: unknown } /** @@ -47,12 +40,10 @@ function normalizeGitHubUser(user: GitHubUser): OAuthUser { } } -function normalizeGitHubTokens(tokens: GitHubTokens): OAuthTokens { +function normalizeGitHubToken(tokens: OAuthAccessTokenSuccess): OAuthToken { return { token: tokens.access_token, - refreshToken: '', - expiresIn: 0, - approvedScopes: tokens.scope.split(','), + approvedScopes: tokens.scope?.split(','), } } @@ -164,17 +155,18 @@ export function oauthGitHubEventHandler({ config, onSuccess, onError }: OAuthCon }, }, ) - if ((tokens as GitHubError).error) { + + if ((tokens as OAuthAccessTokenError).error) { const error = createError({ statusCode: 401, - message: `GitHub login failed: ${(tokens as GitHubError).error || 'Unknown error'}`, - data: tokens as GitHubError, + message: `GitHub login failed: ${(tokens as OAuthAccessTokenError).error || 'Unknown error'}`, + data: tokens as OAuthAccessTokenError, }) if (!onError) throw error return onError(event, error) } - const accessToken = (tokens as GitHubTokens).access_token + const accessToken = (tokens as OAuthAccessTokenSuccess).access_token const user: GitHubUser = await $fetch('https://api.github.com/user', { headers: { 'User-Agent': `Github-OAuth-${config.clientId}`, @@ -200,7 +192,7 @@ export function oauthGitHubEventHandler({ config, onSuccess, onError }: OAuthCon return onSuccess(event, { user: normalizeGitHubUser(user), - tokens: normalizeGitHubTokens(tokens as GitHubTokens), + tokens: normalizeGitHubToken(tokens as OAuthAccessTokenSuccess), }) }) } diff --git a/src/runtime/types/index.ts b/src/runtime/types/index.ts index 5dbbc0d8..d1e27590 100644 --- a/src/runtime/types/index.ts +++ b/src/runtime/types/index.ts @@ -1,3 +1,3 @@ export type { User, UserSession, UserSessionRequired, UserSessionComposable } from './session' export type { OAuthConfig } from './oauth-config' -export type { OAuthTokens, OAuthUser } from './oauth' +export type { OAuthToken, OAuthUser, OAuthAccessTokenError, OAuthAccessTokenSuccess } from './oauth' diff --git a/src/runtime/types/oauth-config.ts b/src/runtime/types/oauth-config.ts index 36d18611..320b83e6 100644 --- a/src/runtime/types/oauth-config.ts +++ b/src/runtime/types/oauth-config.ts @@ -1,11 +1,11 @@ import type { H3Event, H3Error } from 'h3' -import type { OAuthTokens, OAuthUser } from '#auth-utils' +import type { OAuthToken, OAuthUser } from '#auth-utils' export interface OAuthConfig> { config?: TConfig onSuccess: ( event: H3Event, - result: { user: OAuthUser, tokens: OAuthTokens } + result: { user: OAuthUser, tokens: OAuthToken } ) => Promise | void onError?: (event: H3Event, error: H3Error) => Promise | void } diff --git a/src/runtime/types/oauth.ts b/src/runtime/types/oauth.ts index c3f31497..93bb6d88 100644 --- a/src/runtime/types/oauth.ts +++ b/src/runtime/types/oauth.ts @@ -6,19 +6,19 @@ export interface OAuthUser> { /** * The user's username */ - nickname: string + nickname?: string /** * The user's full name */ - name: string + name?: string /** * The user's email address */ - email: string + email?: string /** * The user's profile picture URL */ - avatar: string + avatar?: string /** * The raw user object from the provider @@ -26,21 +26,43 @@ export interface OAuthUser> { raw: RawUser } -export interface OAuthTokens { +export interface OAuthToken { /** * The access token to use for API requests */ - token: string + token?: string /** * The refresh token to use to get a new access token */ - refreshToken: string + refreshToken?: string /** * The token type */ - expiresIn: number + expiresIn?: number /** * The scope of the access token */ - approvedScopes: string[] + approvedScopes?: string[] +} + +/** + * The successful response from the OAuth provider when exchanging a code for an access token. + * @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.1 + */ +export interface OAuthAccessTokenSuccess { + access_token: string + token_type: string + expires_in?: number + refresh_token?: string + scope?: string +} + +/** + * The error response from the OAuth provider when exchanging a code for an access token. + * @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 + */ +export interface OAuthAccessTokenError { + error: string + error_description?: string + error_uri?: string } From 457aff9d4969b18ea840a6dbb6ac7d2d8fc5fa1f Mon Sep 17 00:00:00 2001 From: barbapapazes Date: Sat, 17 Aug 2024 15:36:10 +0200 Subject: [PATCH 3/6] feat: migrate facebook provider --- src/runtime/server/lib/oauth/discord.ts | 4 +- src/runtime/server/lib/oauth/facebook.ts | 61 +++++++++++++++++++----- src/runtime/server/lib/oauth/github.ts | 4 +- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/runtime/server/lib/oauth/discord.ts b/src/runtime/server/lib/oauth/discord.ts index 90b84d9a..8eace574 100644 --- a/src/runtime/server/lib/oauth/discord.ts +++ b/src/runtime/server/lib/oauth/discord.ts @@ -3,7 +3,7 @@ import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from import { withQuery, parseURL, stringifyParsedURL } from 'ufo' import { defu } from 'defu' import { useRuntimeConfig } from '#imports' -import type { OAuthAccessTokenError, OAuthAccessTokenSuccess, OAuthConfig, OAuthToken } from '#auth-utils' +import type { OAuthAccessTokenError, OAuthAccessTokenSuccess, OAuthConfig, OAuthToken, OAuthUser } from '#auth-utils' /** * @see https://discord.com/developers/docs/resources/user#user-object @@ -16,7 +16,7 @@ type DiscordUser = { email?: string } -function normalizeDiscordUser(user: DiscordUser) { +function normalizeDiscordUser(user: DiscordUser): OAuthUser { return { id: user.id, nickname: user.username, diff --git a/src/runtime/server/lib/oauth/facebook.ts b/src/runtime/server/lib/oauth/facebook.ts index 8c4f8fa1..975e7ee5 100644 --- a/src/runtime/server/lib/oauth/facebook.ts +++ b/src/runtime/server/lib/oauth/facebook.ts @@ -9,7 +9,47 @@ import { import { withQuery } from 'ufo' import { defu } from 'defu' import { useRuntimeConfig } from '#imports' -import type { OAuthConfig } from '#auth-utils' +import type { OAuthAccessTokenError, OAuthAccessTokenSuccess, OAuthConfig, OAuthToken, OAuthUser } from '#auth-utils' + +/** + * @see https://developers.facebook.com/docs/graph-api/reference/user + */ +type FacebookUser = { + id: string + name: string + email?: string + // https://developers.facebook.com/docs/graph-api/reference/user/picture/ + picture?: { + data: { + height: number + is_silhouette: boolean + url: string + width: number + } + } + + [key: string]: unknown +} + +function normalizeFacebookUser(user: FacebookUser): OAuthUser { + return { + id: user.id, + nickname: user.name, + name: user.name, + email: user.email, + avatar: user.picture?.data.url, + raw: user, + } +} + +function normalizeFacebookTokens(tokens: OAuthAccessTokenSuccess): OAuthToken { + return { + token: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresIn: tokens.expires_in, + approvedScopes: tokens.scope?.split(','), + } +} export interface OAuthFacebookConfig { /** @@ -109,9 +149,7 @@ export function oauthFacebookEventHandler({ ) } - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch(config.tokenURL as string, { + const tokens = await $fetch(config.tokenURL as string, { method: 'POST', body: { client_id: config.clientId, @@ -120,23 +158,22 @@ export function oauthFacebookEventHandler({ code: query.code, }, }) - if (tokens.error) { + if ((tokens as OAuthAccessTokenError).error) { const error = createError({ statusCode: 401, - message: `Facebook login failed: ${tokens.error || 'Unknown error'}`, - data: tokens, + message: `Facebook login failed: ${(tokens as OAuthAccessTokenError).error || 'Unknown error'}`, + data: tokens as OAuthAccessTokenError, }) if (!onError) throw error return onError(event, error) } - const accessToken = tokens.access_token - // TODO: improve typing + const accessToken = (tokens as OAuthAccessTokenSuccess).access_token config.fields = config.fields || ['id', 'name'] const fields = config.fields.join(',') - const user = await $fetch( + const user = await $fetch( `https://graph.facebook.com/v19.0/me?fields=${fields}&access_token=${accessToken}`, ) @@ -145,8 +182,8 @@ export function oauthFacebookEventHandler({ } return onSuccess(event, { - user, - tokens, + user: normalizeFacebookUser(user), + tokens: normalizeFacebookTokens(tokens as OAuthAccessTokenSuccess), }) }) } diff --git a/src/runtime/server/lib/oauth/github.ts b/src/runtime/server/lib/oauth/github.ts index b6fe77e7..0ee7a9d3 100644 --- a/src/runtime/server/lib/oauth/github.ts +++ b/src/runtime/server/lib/oauth/github.ts @@ -167,7 +167,7 @@ export function oauthGitHubEventHandler({ config, onSuccess, onError }: OAuthCon } const accessToken = (tokens as OAuthAccessTokenSuccess).access_token - const user: GitHubUser = await $fetch('https://api.github.com/user', { + const user = await $fetch('https://api.github.com/user', { headers: { 'User-Agent': `Github-OAuth-${config.clientId}`, 'Authorization': `token ${accessToken}`, @@ -176,7 +176,7 @@ export function oauthGitHubEventHandler({ config, onSuccess, onError }: OAuthCon // if no public email, check the private ones if (!user.email && config.emailRequired) { - const emails: GitHubEmail[] = await $fetch('https://api.github.com/user/emails', { + const emails = await $fetch('https://api.github.com/user/emails', { headers: { 'User-Agent': `Github-OAuth-${config.clientId}`, 'Authorization': `token ${accessToken}`, From 61d7003d4d80251bf981b422010899b491287b32 Mon Sep 17 00:00:00 2001 From: barbapapazes Date: Sat, 17 Aug 2024 15:54:44 +0200 Subject: [PATCH 4/6] feat: migrate steam --- src/runtime/server/lib/oauth/facebook.ts | 4 ++-- src/runtime/server/lib/oauth/steam.ts | 30 ++++++++++++++++++------ src/runtime/types/oauth-config.ts | 2 +- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/runtime/server/lib/oauth/facebook.ts b/src/runtime/server/lib/oauth/facebook.ts index 975e7ee5..1e3f8656 100644 --- a/src/runtime/server/lib/oauth/facebook.ts +++ b/src/runtime/server/lib/oauth/facebook.ts @@ -1,4 +1,4 @@ -import type { H3Event } from 'h3' +import type { H3Event, EventHandler } from 'h3' import { eventHandler, createError, @@ -106,7 +106,7 @@ export function oauthFacebookEventHandler({ config, onSuccess, onError, -}: OAuthConfig) { +}: OAuthConfig): EventHandler { return eventHandler(async (event: H3Event) => { config = defu(config, useRuntimeConfig(event).oauth?.facebook, { authorizationURL: 'https://www.facebook.com/v19.0/dialog/oauth', diff --git a/src/runtime/server/lib/oauth/steam.ts b/src/runtime/server/lib/oauth/steam.ts index 62211840..58a40ede 100644 --- a/src/runtime/server/lib/oauth/steam.ts +++ b/src/runtime/server/lib/oauth/steam.ts @@ -1,9 +1,27 @@ -import type { H3Event } from 'h3' +import type { H3Event, EventHandler } from 'h3' import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from 'h3' import { withQuery } from 'ufo' import { defu } from 'defu' import { useRuntimeConfig } from '#imports' -import type { OAuthConfig } from '#auth-utils' +import type { OAuthConfig, OAuthUser } from '#auth-utils' + +/** + * @see https://partner.steamgames.com/doc/webapi/ISteamUser#GetPlayerSummaries + */ +type SteamUser = { + steamid: string + personaname: string + avatar: string +} + +function normalizeSteamUser(user: SteamUser): OAuthUser { + return { + id: user.steamid, + nickname: user.personaname, + avatar: user.avatar, + raw: user, + } +} export interface OAuthSteamConfig { /** @@ -26,7 +44,7 @@ export interface OAuthSteamConfig { redirectURL?: string } -export function oauthSteamEventHandler({ config, onSuccess, onError }: OAuthConfig) { +export function oauthSteamEventHandler({ config, onSuccess, onError }: OAuthConfig): EventHandler { return eventHandler(async (event: H3Event) => { config = defu(config, useRuntimeConfig(event).oauth?.steam, { authorizationURL: 'https://steamcommunity.com/openid/login', @@ -72,15 +90,13 @@ export function oauthSteamEventHandler({ config, onSuccess, onError }: OAuthConf const steamId = query['openid.claimed_id'].split('/').pop() - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const user: any = await $fetch(withQuery('https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/', { + const user = await $fetch<{ response: { players: SteamUser[] } }>(withQuery('https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/', { key: config.apiKey, steamids: steamId, })) return onSuccess(event, { - user: user.response.players[0], + user: normalizeSteamUser(user.response.players[0]), tokens: null, }) }) diff --git a/src/runtime/types/oauth-config.ts b/src/runtime/types/oauth-config.ts index 320b83e6..46e30f00 100644 --- a/src/runtime/types/oauth-config.ts +++ b/src/runtime/types/oauth-config.ts @@ -5,7 +5,7 @@ export interface OAuthConfig> { config?: TConfig onSuccess: ( event: H3Event, - result: { user: OAuthUser, tokens: OAuthToken } + result: { user: OAuthUser, tokens: OAuthToken | null } ) => Promise | void onError?: (event: H3Event, error: H3Error) => Promise | void } From 729b09876edc021fa5445a6622be56992f249298 Mon Sep 17 00:00:00 2001 From: barbapapazes Date: Sat, 17 Aug 2024 15:58:18 +0200 Subject: [PATCH 5/6] fix: type --- playground/server/routes/auth/steam.get.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/server/routes/auth/steam.get.ts b/playground/server/routes/auth/steam.get.ts index 286e3c80..3e217f8c 100644 --- a/playground/server/routes/auth/steam.get.ts +++ b/playground/server/routes/auth/steam.get.ts @@ -2,7 +2,7 @@ export default oauthSteamEventHandler({ async onSuccess(event, { user }) { await setUserSession(event, { user: { - steam: user.steamid, + steam: user.id as string, }, loggedInAt: Date.now(), }) From f09ded6d2420fa44093e241c093ffa46d0c54a1c Mon Sep 17 00:00:00 2001 From: barbapapazes Date: Mon, 26 Aug 2024 10:41:56 +0200 Subject: [PATCH 6/6] refactor: normalize auth0, battledotnet, cognito --- src/runtime/server/lib/oauth/auth0.ts | 65 ++++++++++++++------ src/runtime/server/lib/oauth/battledotnet.ts | 52 +++++++++++----- src/runtime/server/lib/oauth/cognito.ts | 65 +++++++++++++++----- src/runtime/server/lib/oauth/github.ts | 44 +++++++------ 4 files changed, 158 insertions(+), 68 deletions(-) diff --git a/src/runtime/server/lib/oauth/auth0.ts b/src/runtime/server/lib/oauth/auth0.ts index ca938afe..6f5b09e8 100644 --- a/src/runtime/server/lib/oauth/auth0.ts +++ b/src/runtime/server/lib/oauth/auth0.ts @@ -1,9 +1,23 @@ -import type { H3Event } from 'h3' +import type { H3Event, EventHandler } from 'h3' import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from 'h3' import { withQuery, parsePath } from 'ufo' import { defu } from 'defu' import { useRuntimeConfig } from '#imports' -import type { OAuthConfig } from '#auth-utils' +import type { OAuthAccessTokenError, OAuthAccessTokenSuccess, OAuthConfig, OAuthToken, OAuthUser } from '#auth-utils' + +/** + * Auth0 User + * + * @see https://auth0.com/docs/api/authentication#user-profile + */ +type Auth0User = { + email: string + email_verified: boolean + name: string + picture: string + sub: string + updated_at: string +} export interface OAuthAuth0Config { /** @@ -64,7 +78,7 @@ export interface OAuthAuth0Config { redirectURL?: string } -export function oauthAuth0EventHandler({ config, onSuccess, onError }: OAuthConfig) { +export function oauthAuth0EventHandler({ config, onSuccess, onError }: OAuthConfig): EventHandler { return eventHandler(async (event: H3Event) => { config = defu(config, useRuntimeConfig(event).oauth?.auth0, { authorizationParams: {}, @@ -104,9 +118,7 @@ export function oauthAuth0EventHandler({ config, onSuccess, onError }: OAuthConf ) } - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch( + const tokens = await $fetch( tokenURL as string, { method: 'POST', @@ -121,33 +133,50 @@ export function oauthAuth0EventHandler({ config, onSuccess, onError }: OAuthConf code, }, }, - ).catch((error) => { - return { error } - }) - if (tokens.error) { + ) + + if ((tokens as OAuthAccessTokenError).error) { const error = createError({ statusCode: 401, - message: `Auth0 login failed: ${tokens.error?.data?.error_description || 'Unknown error'}`, + message: `Auth0 login failed: ${(tokens as OAuthAccessTokenError).error || 'Unknown error'}`, data: tokens, }) if (!onError) throw error return onError(event, error) } - const tokenType = tokens.token_type - const accessToken = tokens.access_token + const tokenType = (tokens as OAuthAccessTokenSuccess).token_type + const accessToken = (tokens as OAuthAccessTokenSuccess).access_token - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const user: any = await $fetch(`https://${config.domain}/userinfo`, { + const user = await $fetch(`https://${config.domain}/userinfo`, { headers: { Authorization: `${tokenType} ${accessToken}`, }, }) return onSuccess(event, { - tokens, - user, + user: normalizeAuth0User(user), + tokens: normalizeAuth0Tokens(tokens as OAuthAccessTokenSuccess), }) }) } + +function normalizeAuth0User(user: Auth0User): OAuthUser { + return { + id: user.sub, + nickname: user.name, + name: user.name, + email: user.email, + avatar: user.picture, + raw: user, + } +} + +function normalizeAuth0Tokens(tokens: OAuthAccessTokenSuccess): OAuthToken { + return { + token: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresIn: tokens.expires_in, + approvedScopes: tokens.scope?.split(' ') || [], + } +} diff --git a/src/runtime/server/lib/oauth/battledotnet.ts b/src/runtime/server/lib/oauth/battledotnet.ts index 48ee0102..f3515711 100644 --- a/src/runtime/server/lib/oauth/battledotnet.ts +++ b/src/runtime/server/lib/oauth/battledotnet.ts @@ -1,10 +1,21 @@ import { randomUUID } from 'node:crypto' -import type { H3Event } from 'h3' +import type { H3Event, EventHandler } from 'h3' import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from 'h3' import { withQuery, parsePath } from 'ufo' import { defu } from 'defu' import { useRuntimeConfig } from '#imports' -import type { OAuthConfig } from '#auth-utils' +import type { OAuthConfig, OAuthToken, OAuthUser, OAuthAccessTokenSuccess, OAuthAccessTokenError } from '#auth-utils' + +/** + * Battle.net User + * + * Unable to find documentation on the user object + */ +type BattledotnetUser = { + sub: string + id: number + battletag: string +} export interface OAuthBattledotnetConfig { /** @@ -53,7 +64,7 @@ export interface OAuthBattledotnetConfig { redirectURL?: string } -export function oauthBattledotnetEventHandler({ config, onSuccess, onError }: OAuthConfig) { +export function oauthBattledotnetEventHandler({ config, onSuccess, onError }: OAuthConfig): EventHandler { return eventHandler(async (event: H3Event) => { config = defu(config, useRuntimeConfig(event).oauth?.battledotnet, { authorizationURL: 'https://oauth.battle.net/authorize', @@ -114,9 +125,7 @@ export function oauthBattledotnetEventHandler({ config, onSuccess, onError }: OA const authCode = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64') - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch( + const tokens = await $fetch( config.tokenURL as string, { method: 'POST', @@ -135,21 +144,19 @@ export function oauthBattledotnetEventHandler({ config, onSuccess, onError }: OA return { error } }) - if (tokens.error) { + if ((tokens as OAuthAccessTokenError).error) { const error = createError({ statusCode: 401, - message: `Battle.net login failed: ${tokens.error || 'Unknown error'}`, - data: tokens, + message: `Battle.net login failed: ${(tokens as OAuthAccessTokenError).error || 'Unknown error'}`, + data: tokens as OAuthAccessTokenError, }) if (!onError) throw error return onError(event, error) } - const accessToken = tokens.access_token + const accessToken = (tokens as OAuthAccessTokenSuccess).access_token - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const user: any = await $fetch('https://oauth.battle.net/userinfo', { + const user = await $fetch('https://oauth.battle.net/userinfo', { headers: { 'User-Agent': `Battledotnet-OAuth-${config.clientId}`, 'Authorization': `Bearer ${accessToken}`, @@ -167,8 +174,23 @@ export function oauthBattledotnetEventHandler({ config, onSuccess, onError }: OA } return onSuccess(event, { - user, - tokens, + user: normalizeBattledotnetUser(user), + tokens: normalizeBattledotnetToken(tokens as OAuthAccessTokenSuccess), }) }) } + +function normalizeBattledotnetUser(user: BattledotnetUser): OAuthUser { + return { + id: user.id, + raw: user, + } +} + +function normalizeBattledotnetToken(tokens: OAuthAccessTokenSuccess): OAuthToken { + return { + token: tokens.access_token, + expiresIn: tokens.expires_in, + approvedScopes: tokens.scope?.split(','), + } +} diff --git a/src/runtime/server/lib/oauth/cognito.ts b/src/runtime/server/lib/oauth/cognito.ts index 1ca15874..d76801dd 100644 --- a/src/runtime/server/lib/oauth/cognito.ts +++ b/src/runtime/server/lib/oauth/cognito.ts @@ -1,9 +1,25 @@ -import type { H3Event } from 'h3' +import type { H3Event, EventHandler } from 'h3' import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from 'h3' import { withQuery, parsePath } from 'ufo' import { defu } from 'defu' import { useRuntimeConfig } from '#imports' -import type { OAuthConfig } from '#auth-utils' +import type { OAuthConfig, OAuthToken, OAuthUser, OAuthAccessTokenSuccess, OAuthAccessTokenError } from '#auth-utils' + +/** + * AWS Cognito User + * + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/userinfo-endpoint.html + */ +type CognitoUser = { + sub: string + email_verified: boolean + email: string + username: string + name: string + picture: string + phone_number_verified: boolean + phone_number: string +} export interface OAuthCognitoConfig { /** @@ -43,7 +59,7 @@ export interface OAuthCognitoConfig { redirectURL?: string } -export function oauthCognitoEventHandler({ config, onSuccess, onError }: OAuthConfig) { +export function oauthCognitoEventHandler({ config, onSuccess, onError }: OAuthConfig): EventHandler { return eventHandler(async (event: H3Event) => { config = defu(config, useRuntimeConfig(event).oauth?.cognito, { authorizationParams: {}, @@ -78,9 +94,7 @@ export function oauthCognitoEventHandler({ config, onSuccess, onError }: OAuthCo ) } - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tokens: any = await $fetch( + const tokens = await $fetch( tokenURL as string, { method: 'POST', @@ -93,29 +107,48 @@ export function oauthCognitoEventHandler({ config, onSuccess, onError }: OAuthCo return { error } }) - if (tokens.error) { + if ((tokens as OAuthAccessTokenError).error) { const error = createError({ statusCode: 401, - message: `Cognito login failed: ${tokens.error_description || 'Unknown error'}`, - data: tokens, + message: `Cognito login failed: ${(tokens as OAuthAccessTokenError).error || 'Unknown error'}`, + data: tokens as OAuthAccessTokenError, }) if (!onError) throw error return onError(event, error) } - const tokenType = tokens.token_type - const accessToken = tokens.access_token - // TODO: improve typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const user: any = await $fetch(`https://${config.userPoolId}.auth.${config.region}.amazoncognito.com/oauth2/userInfo`, { + const tokenType = (tokens as OAuthAccessTokenSuccess).token_type + const accessToken = (tokens as OAuthAccessTokenSuccess).access_token + + const user = await $fetch(`https://${config.userPoolId}.auth.${config.region}.amazoncognito.com/oauth2/userInfo`, { headers: { Authorization: `${tokenType} ${accessToken}`, }, }) return onSuccess(event, { - tokens, - user, + user: normalizeCognitoUser(user), + tokens: normalizeCognitoToken(tokens as OAuthAccessTokenSuccess), }) }) } + +function normalizeCognitoUser(user: CognitoUser): OAuthUser { + return { + id: user.sub, + email: user.email, + nickname: user.username, + name: user.name, + avatar: user.picture, + raw: user, + } +} + +function normalizeCognitoToken(tokens: OAuthAccessTokenSuccess): OAuthToken { + return { + token: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresIn: tokens.expires_in, + approvedScopes: tokens.scope?.split(' '), + } +} diff --git a/src/runtime/server/lib/oauth/github.ts b/src/runtime/server/lib/oauth/github.ts index 0ee7a9d3..b0bdc9e9 100644 --- a/src/runtime/server/lib/oauth/github.ts +++ b/src/runtime/server/lib/oauth/github.ts @@ -6,6 +6,8 @@ import { useRuntimeConfig } from '#imports' import type { OAuthConfig, OAuthToken, OAuthUser, OAuthAccessTokenSuccess, OAuthAccessTokenError } from '#auth-utils' /** + * GitHub User + * * @see https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user */ type GitHubUser = { @@ -20,6 +22,8 @@ type GitHubUser = { } /** + * GitHub Email + * * @see https://docs.github.com/en/rest/users/emails?apiVersion=2022-11-28#list-email-addresses-for-the-authenticated-user */ type GitHubEmail = { @@ -29,24 +33,6 @@ type GitHubEmail = { visibility: string | null } -function normalizeGitHubUser(user: GitHubUser): OAuthUser { - return { - id: user.id, - nickname: user.login, - name: user.name, - email: user.email, - avatar: user.avatar_url, - raw: user, - } -} - -function normalizeGitHubToken(tokens: OAuthAccessTokenSuccess): OAuthToken { - return { - token: tokens.access_token, - approvedScopes: tokens.scope?.split(','), - } -} - export interface OAuthGitHubConfig { /** * GitHub OAuth Client ID @@ -154,7 +140,9 @@ export function oauthGitHubEventHandler({ config, onSuccess, onError }: OAuthCon code: query.code, }, }, - ) + ).catch((error) => { + return { error } + }) if ((tokens as OAuthAccessTokenError).error) { const error = createError({ @@ -196,3 +184,21 @@ export function oauthGitHubEventHandler({ config, onSuccess, onError }: OAuthCon }) }) } + +function normalizeGitHubUser(user: GitHubUser): OAuthUser { + return { + id: user.id, + nickname: user.login, + name: user.name, + email: user.email, + avatar: user.avatar_url, + raw: user, + } +} + +function normalizeGitHubToken(tokens: OAuthAccessTokenSuccess): OAuthToken { + return { + token: tokens.access_token, + approvedScopes: tokens.scope?.split(','), + } +}