Skip to content

Commit dd56268

Browse files
devskillprorenogiesatinux
authored
feat: Add Line OAuth provider (#312)
Co-authored-by: wichai.damalee <[email protected]> Co-authored-by: Sébastien Chopin <[email protected]> Co-authored-by: Sébastien Chopin <[email protected]>
1 parent c98ea5d commit dd56268

File tree

8 files changed

+171
-2
lines changed

8 files changed

+171
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ It can also be set using environment variables:
217217
- Hubspot
218218
- Instagram
219219
- Keycloak
220+
- Line
220221
- Linear
221222
- LinkedIn
222223
- Microsoft

playground/.env.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,8 @@ NUXT_OAUTH_STRAVA_CLIENT_SECRET=
100100
# Hubspot
101101
NUXT_OAUTH_HUBSPOT_CLIENT_ID=
102102
NUXT_OAUTH_HUBSPOT_CLIENT_SECRET=
103-
NUXT_OAUTH_HUBSPOT_REDIRECT_URL=
103+
NUXT_OAUTH_HUBSPOT_REDIRECT_URL=
104+
# Line
105+
NUXT_OAUTH_LINE_CLIENT_ID=
106+
NUXT_OAUTH_LINE_CLIENT_SECRET=
107+
NUXT_OAUTH_LINE_REDIRECT_URL=

playground/app.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ const providers = computed(() =>
3333
disabled: Boolean(user.value?.gitlab),
3434
icon: 'i-simple-icons-gitlab',
3535
},
36+
{
37+
label: user.value?.line || 'Line',
38+
to: '/auth/line',
39+
disabled: Boolean(user.value?.line),
40+
icon: 'i-simple-icons-line',
41+
},
3642
{
3743
label: user.value?.linear || 'Linear',
3844
to: '/auth/linear',

playground/auth.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ declare module '#auth-utils' {
1313
discord?: string
1414
battledotnet?: string
1515
keycloak?: string
16+
line?: string
1617
linear?: string
1718
linkedin?: string
1819
cognito?: string
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export default defineOAuthLineEventHandler({
2+
async onSuccess(event, { user }) {
3+
await setUserSession(event, {
4+
user: {
5+
line: user.userId,
6+
},
7+
loggedInAt: Date.now(),
8+
});
9+
10+
return sendRedirect(event, '/');
11+
},
12+
});

src/module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,5 +355,11 @@ export default defineNuxtModule<ModuleOptions>({
355355
clientSecret: '',
356356
redirectURL: '',
357357
})
358+
// Line OAuth
359+
runtimeConfig.oauth.line = defu(runtimeConfig.oauth.line, {
360+
clientId: '',
361+
clientSecret: '',
362+
redirectURL: '',
363+
})
358364
},
359365
})

src/runtime/server/lib/oauth/line.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import type { H3Event } from 'h3'
2+
import { eventHandler, getQuery, sendRedirect, createError } from 'h3'
3+
import { withQuery } from 'ufo'
4+
import { defu } from 'defu'
5+
import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils'
6+
import { useRuntimeConfig } from '#imports'
7+
import type { OAuthConfig } from '#auth-utils'
8+
9+
export interface OAuthLineConfig {
10+
/**
11+
* Line OAuth Client ID
12+
* @default process.env.NUXT_OAUTH_LINE_CLIENT_ID
13+
*/
14+
clientId?: string
15+
16+
/**
17+
* Line OAuth Client Secret
18+
* @default process.env.NUXT_OAUTH_LINE_CLIENT_SECRET
19+
*/
20+
clientSecret?: string
21+
22+
/**
23+
* Line OAuth Scope
24+
* @default ['profile', 'openid']
25+
* @see https://developers.line.biz/en/docs/line-login/integrate-line-login/
26+
*/
27+
scope?: string[]
28+
29+
/**
30+
* Line OAuth Authorization URL
31+
* @default 'https://access.line.me/oauth2/v2.1/authorize'
32+
*/
33+
authorizationURL?: string
34+
35+
/**
36+
* Line OAuth Token URL
37+
* @default 'https://api.line.me/oauth2/v2.1/token'
38+
*/
39+
tokenURL?: string
40+
41+
/**
42+
* Line OAuth User Info URL
43+
* @default 'https://api.line.me/v2/profile'
44+
*/
45+
userURL?: string
46+
47+
/**
48+
* Extra authorization parameters to provide to the authorization URL
49+
* @example { bot_prompt: 'normal' }
50+
*/
51+
authorizationParams?: Record<string, string>
52+
53+
/**
54+
* Redirect URL to to allow overriding for situations like prod failing to determine public hostname
55+
* @default process.env.NUXT_OAUTH_LINE_REDIRECT_URL or current URL
56+
*/
57+
redirectURL?: string
58+
}
59+
60+
export function defineOAuthLineEventHandler({
61+
config,
62+
onSuccess,
63+
onError,
64+
}: OAuthConfig<OAuthLineConfig>) {
65+
return eventHandler(async (event: H3Event) => {
66+
config = defu(config, useRuntimeConfig(event).oauth?.line, {
67+
authorizationURL: 'https://access.line.me/oauth2/v2.1/authorize',
68+
tokenURL: 'https://api.line.me/oauth2/v2.1/token',
69+
userURL: 'https://api.line.me/v2/profile',
70+
authorizationParams: {},
71+
}) as OAuthLineConfig
72+
73+
const query = getQuery<{ code?: string, error?: string, state?: string }>(event)
74+
75+
if (query.error) {
76+
return onError(
77+
event,
78+
new Error(`Line login failed: ${query.error || 'Unknown error'}`),
79+
)
80+
}
81+
82+
if (!config.clientId || !config.clientSecret) {
83+
return handleMissingConfiguration(event, 'line', ['clientId', 'clientSecret'], onError)
84+
}
85+
86+
const redirectURL = config.redirectURL || getOAuthRedirectURL(event)
87+
if (!query.code) {
88+
config.scope = config.scope || ['profile', 'openid']
89+
// Redirect to Line OAuth page
90+
return sendRedirect(
91+
event,
92+
withQuery(config.authorizationURL as string, {
93+
response_type: 'code',
94+
client_id: config.clientId,
95+
redirect_uri: redirectURL,
96+
scope: config.scope.join(' '),
97+
state: query.state || '',
98+
...config.authorizationParams,
99+
}),
100+
)
101+
}
102+
103+
const tokens = await requestAccessToken(config.tokenURL as string, {
104+
body: {
105+
grant_type: 'authorization_code',
106+
code: query.code as string,
107+
client_id: config.clientId,
108+
client_secret: config.clientSecret,
109+
redirect_uri: redirectURL,
110+
},
111+
})
112+
113+
if (tokens.error) {
114+
return handleAccessTokenErrorResponse(event, 'line', tokens, onError)
115+
}
116+
117+
const accessToken = tokens.access_token
118+
const user = await $fetch(config.userURL as string, {
119+
headers: {
120+
Authorization: `Bearer ${accessToken}`,
121+
},
122+
})
123+
124+
if (!user) {
125+
const error = createError({
126+
statusCode: 500,
127+
message: 'Could not get Line user',
128+
data: tokens,
129+
})
130+
if (!onError) throw error
131+
return onError(event, error)
132+
}
133+
134+
return onSuccess(event, {
135+
tokens,
136+
user,
137+
})
138+
})
139+
}

src/runtime/types/oauth-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { H3Event, H3Error } from 'h3'
22

3-
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 & {})
3+
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 & {})
44

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

0 commit comments

Comments
 (0)