Skip to content

Commit

Permalink
feat: Add Line OAuth provider (#312)
Browse files Browse the repository at this point in the history
Co-authored-by: wichai.damalee <[email protected]>
Co-authored-by: Sébastien Chopin <[email protected]>
Co-authored-by: Sébastien Chopin <[email protected]>
  • Loading branch information
4 people authored Jan 21, 2025
1 parent c98ea5d commit dd56268
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ It can also be set using environment variables:
- Hubspot
- Instagram
- Keycloak
- Line
- Linear
- LinkedIn
- Microsoft
Expand Down
6 changes: 5 additions & 1 deletion playground/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,8 @@ NUXT_OAUTH_STRAVA_CLIENT_SECRET=
# Hubspot
NUXT_OAUTH_HUBSPOT_CLIENT_ID=
NUXT_OAUTH_HUBSPOT_CLIENT_SECRET=
NUXT_OAUTH_HUBSPOT_REDIRECT_URL=
NUXT_OAUTH_HUBSPOT_REDIRECT_URL=
# Line
NUXT_OAUTH_LINE_CLIENT_ID=
NUXT_OAUTH_LINE_CLIENT_SECRET=
NUXT_OAUTH_LINE_REDIRECT_URL=
6 changes: 6 additions & 0 deletions playground/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ const providers = computed(() =>
disabled: Boolean(user.value?.gitlab),
icon: 'i-simple-icons-gitlab',
},
{
label: user.value?.line || 'Line',
to: '/auth/line',
disabled: Boolean(user.value?.line),
icon: 'i-simple-icons-line',
},
{
label: user.value?.linear || 'Linear',
to: '/auth/linear',
Expand Down
1 change: 1 addition & 0 deletions playground/auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ declare module '#auth-utils' {
discord?: string
battledotnet?: string
keycloak?: string
line?: string
linear?: string
linkedin?: string
cognito?: string
Expand Down
12 changes: 12 additions & 0 deletions playground/server/routes/auth/line.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default defineOAuthLineEventHandler({
async onSuccess(event, { user }) {
await setUserSession(event, {
user: {
line: user.userId,
},
loggedInAt: Date.now(),
});

Check failure on line 8 in playground/server/routes/auth/line.get.ts

View workflow job for this annotation

GitHub Actions / lint

Extra semicolon

return sendRedirect(event, '/');

Check failure on line 10 in playground/server/routes/auth/line.get.ts

View workflow job for this annotation

GitHub Actions / lint

Extra semicolon
},
});

Check failure on line 12 in playground/server/routes/auth/line.get.ts

View workflow job for this annotation

GitHub Actions / lint

Extra semicolon
6 changes: 6 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,5 +355,11 @@ export default defineNuxtModule<ModuleOptions>({
clientSecret: '',
redirectURL: '',
})
// Line OAuth
runtimeConfig.oauth.line = defu(runtimeConfig.oauth.line, {
clientId: '',
clientSecret: '',
redirectURL: '',
})
},
})
139 changes: 139 additions & 0 deletions src/runtime/server/lib/oauth/line.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import type { H3Event } from 'h3'
import { eventHandler, getQuery, sendRedirect, createError } from 'h3'
import { withQuery } from 'ufo'
import { defu } from 'defu'
import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils'
import { useRuntimeConfig } from '#imports'
import type { OAuthConfig } from '#auth-utils'

export interface OAuthLineConfig {
/**
* Line OAuth Client ID
* @default process.env.NUXT_OAUTH_LINE_CLIENT_ID
*/
clientId?: string

/**
* Line OAuth Client Secret
* @default process.env.NUXT_OAUTH_LINE_CLIENT_SECRET
*/
clientSecret?: string

/**
* Line OAuth Scope
* @default ['profile', 'openid']
* @see https://developers.line.biz/en/docs/line-login/integrate-line-login/
*/
scope?: string[]

/**
* Line OAuth Authorization URL
* @default 'https://access.line.me/oauth2/v2.1/authorize'
*/
authorizationURL?: string

/**
* Line OAuth Token URL
* @default 'https://api.line.me/oauth2/v2.1/token'
*/
tokenURL?: string

/**
* Line OAuth User Info URL
* @default 'https://api.line.me/v2/profile'
*/
userURL?: string

/**
* Extra authorization parameters to provide to the authorization URL
* @example { bot_prompt: 'normal' }
*/
authorizationParams?: Record<string, string>

/**
* Redirect URL to to allow overriding for situations like prod failing to determine public hostname
* @default process.env.NUXT_OAUTH_LINE_REDIRECT_URL or current URL
*/
redirectURL?: string
}

export function defineOAuthLineEventHandler({
config,
onSuccess,
onError,
}: OAuthConfig<OAuthLineConfig>) {
return eventHandler(async (event: H3Event) => {
config = defu(config, useRuntimeConfig(event).oauth?.line, {
authorizationURL: 'https://access.line.me/oauth2/v2.1/authorize',
tokenURL: 'https://api.line.me/oauth2/v2.1/token',
userURL: 'https://api.line.me/v2/profile',
authorizationParams: {},
}) as OAuthLineConfig

const query = getQuery<{ code?: string, error?: string, state?: string }>(event)

if (query.error) {
return onError(
event,
new Error(`Line login failed: ${query.error || 'Unknown error'}`),
)
}

if (!config.clientId || !config.clientSecret) {
return handleMissingConfiguration(event, 'line', ['clientId', 'clientSecret'], onError)
}

const redirectURL = config.redirectURL || getOAuthRedirectURL(event)
if (!query.code) {
config.scope = config.scope || ['profile', 'openid']
// Redirect to Line OAuth page
return sendRedirect(
event,
withQuery(config.authorizationURL as string, {
response_type: 'code',
client_id: config.clientId,
redirect_uri: redirectURL,
scope: config.scope.join(' '),
state: query.state || '',
...config.authorizationParams,
}),
)
}

const tokens = await requestAccessToken(config.tokenURL as string, {
body: {
grant_type: 'authorization_code',
code: query.code as string,
client_id: config.clientId,
client_secret: config.clientSecret,
redirect_uri: redirectURL,
},
})

if (tokens.error) {
return handleAccessTokenErrorResponse(event, 'line', tokens, onError)
}

const accessToken = tokens.access_token
const user = await $fetch(config.userURL as string, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})

if (!user) {
const error = createError({
statusCode: 500,
message: 'Could not get Line user',
data: tokens,
})
if (!onError) throw error
return onError(event, error)
}

return onSuccess(event, {
tokens,
user,
})
})
}
2 changes: 1 addition & 1 deletion src/runtime/types/oauth-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { H3Event, H3Error } from 'h3'

export type OAuthProvider = 'auth0' | 'authentik' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'github' | 'gitlab' | 'google' | 'hubspot' | 'instagram' | 'keycloak' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'seznam' | 'steam' | 'strava' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | (string & {})
export type OAuthProvider = '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' | (string & {})

export type OnError = (event: H3Event, error: H3Error) => Promise<void> | void

Expand Down

0 comments on commit dd56268

Please sign in to comment.