From 2d478c2d40441e44ef0a3a3eda90b4a97296e242 Mon Sep 17 00:00:00 2001 From: h+ Date: Mon, 3 Feb 2025 23:08:44 +0100 Subject: [PATCH 1/4] feat: Added Gitea Oauth Provider --- playground/.env.example | 3 + playground/auth.d.ts | 1 + playground/server/routes/auth/gitea.get.ts | 15 ++ src/module.ts | 7 + src/runtime/server/lib/oauth/gitea.ts | 154 +++++++++++++++++++++ src/runtime/types/oauth-config.ts | 2 +- 6 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 playground/server/routes/auth/gitea.get.ts create mode 100644 src/runtime/server/lib/oauth/gitea.ts diff --git a/playground/.env.example b/playground/.env.example index 0f1d0b98..34bed41e 100644 --- a/playground/.env.example +++ b/playground/.env.example @@ -5,6 +5,9 @@ NUXT_OAUTH_GITHUB_CLIENT_SECRET= # GitLab OAuth NUXT_OAUTH_GITLAB_CLIENT_ID= NUXT_OAUTH_GITLAB_CLIENT_SECRET= +# Gitea OAuth +NUXT_OAUTH_GITEA_CLIENT_ID= +NUXT_OAUTH_GITEA_CLIENT_SECRET= # Spotify OAuth NUXT_OAUTH_SPOTIFY_CLIENT_ID= NUXT_OAUTH_SPOTIFY_CLIENT_SECRET= diff --git a/playground/auth.d.ts b/playground/auth.d.ts index 5d15cc02..68307f57 100644 --- a/playground/auth.d.ts +++ b/playground/auth.d.ts @@ -4,6 +4,7 @@ declare module '#auth-utils' { email?: string password?: string spotify?: string + gitea?: string github?: string gitlab?: string google?: string diff --git a/playground/server/routes/auth/gitea.get.ts b/playground/server/routes/auth/gitea.get.ts new file mode 100644 index 00000000..db92c3be --- /dev/null +++ b/playground/server/routes/auth/gitea.get.ts @@ -0,0 +1,15 @@ +export default defineOAuthGiteaEventHandler({ + config: { + emailRequired: true + }, + async onSuccess(event, { user }) { + await setUserSession(event, { + user: { + gitea: user.email, + }, + loggedInAt: Date.now(), + }) + return sendRedirect(event, '/') + }, + +}) diff --git a/src/module.ts b/src/module.ts index 94fac667..cf6b14a7 100644 --- a/src/module.ts +++ b/src/module.ts @@ -161,6 +161,13 @@ export default defineNuxtModule({ // OAuth settings runtimeConfig.oauth = defu(runtimeConfig.oauth, {}) + // Gitea OAuth + runtimeConfig.oauth.gitea = defu(runtimeConfig.oauth.gitea, { + clientId: '', + clientSecret: '', + redirectURL: '', + baseURL: '', + }) // GitHub OAuth runtimeConfig.oauth.github = defu(runtimeConfig.oauth.github, { clientId: '', diff --git a/src/runtime/server/lib/oauth/gitea.ts b/src/runtime/server/lib/oauth/gitea.ts new file mode 100644 index 00000000..54288442 --- /dev/null +++ b/src/runtime/server/lib/oauth/gitea.ts @@ -0,0 +1,154 @@ +import type { H3Event } from 'h3' +import { eventHandler, getQuery, sendRedirect } from 'h3' +import { withQuery } from 'ufo' +import { defu } from 'defu' +import { + handleMissingConfiguration, + handleAccessTokenErrorResponse, + getOAuthRedirectURL, + requestAccessToken, +} from '../utils' +import { useRuntimeConfig, createError } from '#imports' +import type { OAuthConfig } from '#auth-utils' + +export interface OAuthGiteaConfig { + /** + * Gitea OAuth Client ID + * @default process.env.NUXT_OAUTH_GITEA_CLIENT_ID + */ + clientId?: string + /** + * Gitea OAuth Client Secret + * @default process.env.NUXT_OAUTH_GITEA_CLIENT_SECRET + */ + clientSecret?: string + /** + * Gitea OAuth Scope + * @default ['read:user'] + * @see https://docs.gitea.io/en-us/oauth2-provider/ + * @example ['read:user'] + */ + scope?: string[] + /** + * Require email from user, adds the ['email'] scope if not present + * @default false + */ + emailRequired?: boolean + + /** + * Gitea OAuth Authorization URL + * @default '/login/oauth/authorize' + */ + authorizationURL?: string + + /** + * Gitea OAuth Token URL + * @default '/login/oauth/access_token' + */ + tokenURL?: string + + /** + * Extra authorization parameters to provide to the authorization URL + * @see https://docs.gitea.io/en-us/oauth2-provider/ + */ + authorizationParams?: Record + + /** + * Redirect URL to to allow overriding for situations like prod failing to determine public hostname + * @default process.env.NUXT_OAUTH_GITEA_REDIRECT_URL + */ + redirectURL?: string + + /** + * URL of your Gitea instance + * @default 'http://localhost:3000' + */ + baseURL?: string +} + +export function defineOAuthGiteaEventHandler({ + config, + onSuccess, + onError +}: OAuthConfig) { + return eventHandler(async (event: H3Event) => { + const runtimeConfig = useRuntimeConfig(event).oauth?.gitea + const baseURL = config?.baseURL ?? runtimeConfig.baseURL ?? 'http://localhost:3000' + config = defu(config, runtimeConfig, { + authorizationURL: `${baseURL}/login/oauth/authorize`, + tokenURL: `${baseURL}/login/oauth/access_token`, + authorizationParams: {} + }) as OAuthGiteaConfig + + const query = getQuery<{ code?: string, error?: string }>(event) + + if (query.error) { + const error = createError({ + statusCode: 401, + message: `Gitea login failed: ${query.error || 'Unknown error'}`, + data: query + }) + if (!onError) throw error + return onError(event, error) + } + + if (!config.clientId || !config.clientSecret) { + return handleMissingConfiguration( + event, + 'gitea', + ['clientId', 'clientSecret'], + onError + ) + } + + const redirectURL = config.redirectURL || getOAuthRedirectURL(event) + + if (!query.code) { + config.scope = config.scope || [] + if (!config.scope.length) { + config.scope.push('read:user') + } + if (config.emailRequired && !config.scope.includes('email')) { + config.scope.push('email') + } + + return sendRedirect( + event, + withQuery(config.authorizationURL as string, { + response_type: 'code', + client_id: config.clientId, + redirect_uri: redirectURL, + scope: config.scope.join(' '), + ...config.authorizationParams + }) + ) + } + + const tokens = await requestAccessToken(config.tokenURL as string, { + body: { + grant_type: 'authorization_code', + client_id: config.clientId, + client_secret: config.clientSecret, + redirect_uri: redirectURL, + code: query.code + } + }) + + if (tokens.error) { + return handleAccessTokenErrorResponse(event, 'gitea', tokens, onError) + } + + const accessToken = tokens.access_token + + const user: any = await $fetch(`${baseURL}/api/v1/user`, { + headers: { + Authorization: `token ${accessToken}` + } + }) + + return onSuccess(event, { + user, + tokens + }) + }) +} diff --git a/src/runtime/types/oauth-config.ts b/src/runtime/types/oauth-config.ts index 079e2862..58031c6c 100644 --- a/src/runtime/types/oauth-config.ts +++ b/src/runtime/types/oauth-config.ts @@ -1,6 +1,6 @@ import type { H3Event, H3Error } from 'h3' -export type OAuthProvider = 'atlassian' | 'auth0' | 'authentik' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'github' | 'gitlab' | 'google' | 'hubspot' | 'instagram' | 'keycloak' | 'line' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'seznam' | 'steam' | 'strava' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | 'apple' | (string & {}) +export type OAuthProvider = 'atlassian' | 'auth0' | 'authentik' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'gitea' | 'github' | 'gitlab' | 'google' | 'hubspot' | 'instagram' | 'keycloak' | 'line' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'seznam' | 'steam' | 'strava' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | 'apple' | (string & {}) export type OnError = (event: H3Event, error: H3Error) => Promise | void From d2d32fdeaf828f332e4533df987eabfbe5526dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Wed, 5 Feb 2025 00:54:53 +0100 Subject: [PATCH 2/4] up --- src/runtime/server/lib/oauth/gitea.ts | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/runtime/server/lib/oauth/gitea.ts b/src/runtime/server/lib/oauth/gitea.ts index 54288442..56049933 100644 --- a/src/runtime/server/lib/oauth/gitea.ts +++ b/src/runtime/server/lib/oauth/gitea.ts @@ -61,7 +61,7 @@ export interface OAuthGiteaConfig { /** * URL of your Gitea instance - * @default 'http://localhost:3000' + * @default 'https://gitea.com' */ baseURL?: string } @@ -69,15 +69,15 @@ export interface OAuthGiteaConfig { export function defineOAuthGiteaEventHandler({ config, onSuccess, - onError + onError, }: OAuthConfig) { return eventHandler(async (event: H3Event) => { const runtimeConfig = useRuntimeConfig(event).oauth?.gitea - const baseURL = config?.baseURL ?? runtimeConfig.baseURL ?? 'http://localhost:3000' + const baseURL = config?.baseURL ?? runtimeConfig.baseURL ?? 'https://gitea.com' config = defu(config, runtimeConfig, { authorizationURL: `${baseURL}/login/oauth/authorize`, tokenURL: `${baseURL}/login/oauth/access_token`, - authorizationParams: {} + authorizationParams: {}, }) as OAuthGiteaConfig const query = getQuery<{ code?: string, error?: string }>(event) @@ -86,7 +86,7 @@ export function defineOAuthGiteaEventHandler({ const error = createError({ statusCode: 401, message: `Gitea login failed: ${query.error || 'Unknown error'}`, - data: query + data: query, }) if (!onError) throw error return onError(event, error) @@ -97,7 +97,7 @@ export function defineOAuthGiteaEventHandler({ event, 'gitea', ['clientId', 'clientSecret'], - onError + onError, ) } @@ -119,8 +119,8 @@ export function defineOAuthGiteaEventHandler({ client_id: config.clientId, redirect_uri: redirectURL, scope: config.scope.join(' '), - ...config.authorizationParams - }) + ...config.authorizationParams, + }), ) } @@ -130,8 +130,8 @@ export function defineOAuthGiteaEventHandler({ client_id: config.clientId, client_secret: config.clientSecret, redirect_uri: redirectURL, - code: query.code - } + code: query.code, + }, }) if (tokens.error) { @@ -140,15 +140,16 @@ export function defineOAuthGiteaEventHandler({ const accessToken = tokens.access_token + // eslint-disable-next-line @typescript-eslint/no-explicit-any const user: any = await $fetch(`${baseURL}/api/v1/user`, { headers: { - Authorization: `token ${accessToken}` - } + Authorization: `token ${accessToken}`, + }, }) return onSuccess(event, { user, - tokens + tokens, }) }) } From 52cf26a21ac02aa7b2f4fb3ef77fe7dcdd2c0022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Wed, 5 Feb 2025 00:55:48 +0100 Subject: [PATCH 3/4] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 46563564..34be6073 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,7 @@ It can also be set using environment variables: - Facebook - GitHub - GitLab +- Gitea - Google - Hubspot - Instagram From 32d56aeb0898d2ac3561da5a622c1773dda5afc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Wed, 5 Feb 2025 00:56:22 +0100 Subject: [PATCH 4/4] fix lint --- playground/server/routes/auth/gitea.get.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/server/routes/auth/gitea.get.ts b/playground/server/routes/auth/gitea.get.ts index db92c3be..af670dcd 100644 --- a/playground/server/routes/auth/gitea.get.ts +++ b/playground/server/routes/auth/gitea.get.ts @@ -1,6 +1,6 @@ export default defineOAuthGiteaEventHandler({ config: { - emailRequired: true + emailRequired: true, }, async onSuccess(event, { user }) { await setUserSession(event, {