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/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/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(), }) 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/discord.ts b/src/runtime/server/lib/oauth/discord.ts index 27baaee3..8eace574 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, OAuthUser } 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): OAuthUser { + 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/facebook.ts b/src/runtime/server/lib/oauth/facebook.ts index 8c4f8fa1..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, @@ -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 { /** @@ -66,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', @@ -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 e949e672..b0bdc9e9 100644 --- a/src/runtime/server/lib/oauth/github.ts +++ b/src/runtime/server/lib/oauth/github.ts @@ -1,9 +1,37 @@ -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, 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 = { + login: string + id: number + node_id: string + avatar_url: string + name: string + email: string + + [key: string]: unknown +} + +/** + * GitHub Email + * + * @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 +} export interface OAuthGitHubConfig { /** @@ -56,7 +84,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 +130,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', @@ -114,21 +140,22 @@ export function oauthGitHubEventHandler({ config, onSuccess, onError }: OAuthCon code: query.code, }, }, - ) - if (tokens.error) { + ).catch((error) => { + return { error } + }) + + if ((tokens as OAuthAccessTokenError).error) { const error = createError({ statusCode: 401, - message: `GitHub login failed: ${tokens.error || 'Unknown error'}`, - data: tokens, + 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.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 OAuthAccessTokenSuccess).access_token + const user = await $fetch('https://api.github.com/user', { headers: { 'User-Agent': `Github-OAuth-${config.clientId}`, 'Authorization': `token ${accessToken}`, @@ -137,17 +164,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 = 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 +179,26 @@ export function oauthGitHubEventHandler({ config, onSuccess, onError }: OAuthCon } return onSuccess(event, { - user, - tokens, + user: normalizeGitHubUser(user), + tokens: normalizeGitHubToken(tokens as OAuthAccessTokenSuccess), }) }) } + +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(','), + } +} 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/index.ts b/src/runtime/types/index.ts index 2a759eb9..d1e27590 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 { OAuthToken, OAuthUser, OAuthAccessTokenError, OAuthAccessTokenSuccess } from './oauth' diff --git a/src/runtime/types/oauth-config.ts b/src/runtime/types/oauth-config.ts index c4c07c9d..46e30f00 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 { OAuthToken, 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: OAuthToken | null } ) => 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..93bb6d88 --- /dev/null +++ b/src/runtime/types/oauth.ts @@ -0,0 +1,68 @@ +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 OAuthToken { + /** + * 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[] +} + +/** + * 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 +}