Skip to content

Commit cf89c3c

Browse files
phofhiddenshapeatinux
authored
feat: add Salesforce, Slack and Heroku OAuth providers (#382)
Co-authored-by: Alessandro <[email protected]> Co-authored-by: Sébastien Chopin <[email protected]>
1 parent bb02f4a commit cf89c3c

File tree

12 files changed

+486
-2
lines changed

12 files changed

+486
-2
lines changed

README.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Add Authentication to Nuxt applications with secured & sealed cookies sessions.
1616
## Features
1717

1818
- [Hybrid Rendering](#hybrid-rendering) support (SSR / CSR / SWR / Prerendering)
19-
- [30+ OAuth Providers](#supported-oauth-providers)
19+
- [40+ OAuth Providers](#supported-oauth-providers)
2020
- [Password Hashing](#password-hashing)
2121
- [WebAuthn (passkey)](#webauthn-passkey)
2222
- [`useUserSession()` Vue composable](#vue-composable)
@@ -226,6 +226,7 @@ It can also be set using environment variables:
226226
- GitLab
227227
- Gitea
228228
- Google
229+
- Heroku
229230
- Hubspot
230231
- Instagram
231232
- Kick
@@ -237,7 +238,9 @@ It can also be set using environment variables:
237238
- Microsoft
238239
- PayPal
239240
- Polar
241+
- Salesforce
240242
- Seznam
243+
- Slack
241244
- Spotify
242245
- Steam
243246
- Strava

playground/.env.example

+12
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,15 @@ NUXT_OAUTH_KICK_REDIRECT_URL=
125125
#LiveChat
126126
NUXT_OAUTH_LIVECHAT_CLIENT_ID=
127127
NUXT_OAUTH_LIVECHAT_CLIENT_SECRET=
128+
#Salesforce
129+
NUXT_OAUTH_SALESFORCE_CLIENT_ID=
130+
NUXT_OAUTH_SALESFORCE_CLIENT_SECRET=
131+
NUXT_OAUTH_SALESFORCE_REDIRECT_URL=
132+
#Slack
133+
NUXT_OAUTH_SLACK_CLIENT_ID=
134+
NUXT_OAUTH_SLACK_CLIENT_SECRET=
135+
NUXT_OAUTH_SLACK_REDIRECT_URL=
136+
#Heroku
137+
NUXT_OAUTH_HEROKU_CLIENT_ID=
138+
NUXT_OAUTH_HEROKU_CLIENT_SECRET=
139+
NUXT_OAUTH_HEROKU_REDIRECT_URL=

playground/app.vue

+18
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,24 @@ const providers = computed(() =>
230230
disabled: Boolean(user.value?.kick),
231231
icon: 'i-simple-icons-kick',
232232
},
233+
{
234+
label: user.value?.salesforce || 'Salesforce',
235+
to: `/auth/salesforce`,
236+
disabled: Boolean(user.value?.salesforce),
237+
icon: 'i-simple-icons-salesforce',
238+
},
239+
{
240+
label: user.value?.slack || 'Slack',
241+
to: '/auth/slack',
242+
disabled: Boolean(user.value?.slack),
243+
icon: 'i-simple-icons-slack',
244+
},
245+
{
246+
label: user.value?.heroku || 'Heroku',
247+
to: '/auth/heroku',
248+
disabled: Boolean(user.value?.heroku),
249+
icon: 'i-simple-icons-heroku',
250+
},
233251
].map(p => ({
234252
...p,
235253
prefetch: false,

playground/auth.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ declare module '#auth-utils' {
4040
apple?: string
4141
azureb2c?: string
4242
kick?: string
43+
salesforce?: string
44+
slack?: string
45+
heroku?: string
4346
}
4447

4548
interface UserSession {
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export default defineOAuthHerokuEventHandler({
2+
config: {},
3+
async onSuccess(event, { user }) {
4+
await setUserSession(event, {
5+
user: {
6+
heroku: user?.name,
7+
},
8+
loggedInAt: Date.now(),
9+
})
10+
11+
return sendRedirect(event, '/')
12+
},
13+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export default defineOAuthSalesforceEventHandler({
2+
config: {},
3+
async onSuccess(event, { user }) {
4+
await setUserSession(event, {
5+
user: {
6+
salesforce: user?.name,
7+
},
8+
loggedInAt: Date.now(),
9+
})
10+
11+
return sendRedirect(event, '/')
12+
},
13+
})
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export default defineOAuthSlackEventHandler({
2+
config: {},
3+
async onSuccess(event, { user }) {
4+
await setUserSession(event, {
5+
user: {
6+
slack: user?.name,
7+
},
8+
loggedInAt: Date.now(),
9+
})
10+
11+
return sendRedirect(event, '/')
12+
},
13+
})

src/module.ts

+22
Original file line numberDiff line numberDiff line change
@@ -446,5 +446,27 @@ export default defineNuxtModule<ModuleOptions>({
446446
clientId: '',
447447
clientSecret: '',
448448
})
449+
// Salesforce OAuth
450+
runtimeConfig.oauth.salesforce = defu(runtimeConfig.oauth.salesforce, {
451+
clientId: '',
452+
clientSecret: '',
453+
redirectURL: '',
454+
baseURL: '',
455+
scope: '',
456+
})
457+
// Slack OAuth
458+
runtimeConfig.oauth.slack = defu(runtimeConfig.oauth.slack, {
459+
clientId: '',
460+
clientSecret: '',
461+
redirectURL: '',
462+
scope: '',
463+
})
464+
// Heroku OAuth
465+
runtimeConfig.oauth.heroku = defu(runtimeConfig.oauth.heroku, {
466+
clientId: '',
467+
clientSecret: '',
468+
redirectURL: '',
469+
scope: '',
470+
})
449471
},
450472
})
+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import type { H3Event } from 'h3'
2+
import { eventHandler, getQuery, sendRedirect } from 'h3'
3+
import { withQuery } from 'ufo'
4+
import { defu } from 'defu'
5+
import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken, handleState, handleInvalidState } from '../utils'
6+
import { useRuntimeConfig, createError } from '#imports'
7+
import type { OAuthConfig } from '#auth-utils'
8+
9+
export interface OAuthHerokuConfig {
10+
/**
11+
* Heroku OAuth Client ID
12+
* @default process.env.NUXT_OAUTH_HEROKU_CLIENT_ID
13+
*/
14+
clientId?: string
15+
/**
16+
* Heroku OAuth Client Secret
17+
* @default process.env.NUXT_OAUTH_HEROKU_CLIENT_SECRET
18+
*/
19+
clientSecret?: string
20+
/**
21+
* Heroku OAuth Scope
22+
* @default ['identity']
23+
* @see https://devcenter.heroku.com/articles/oauth#scopes
24+
* @example ['identity']
25+
*/
26+
scope?: string[]
27+
/**
28+
* Heroku OAuth Authorization URL
29+
* @default 'https://id.heroku.com/oauth/authorize'
30+
*/
31+
authorizationURL?: string
32+
/**
33+
* Heroku OAuth Authorization URL
34+
* @default 'https://id.heroku.com/oauth/token'
35+
*/
36+
tokenURL?: string
37+
/**
38+
* Extra authorization parameters to provide to the authorization URL
39+
* @default {}
40+
*/
41+
authorizationParams?: Record<string, string>
42+
/**
43+
* Redirect URL to allow overriding for situations like prod failing to determine public hostname
44+
* @default process.env.NUXT_OAUTH_HEROKU_REDIRECT_URL or current URL
45+
*/
46+
redirectURL?: string
47+
}
48+
49+
export function defineOAuthHerokuEventHandler({
50+
config,
51+
onSuccess,
52+
onError,
53+
}: OAuthConfig<OAuthHerokuConfig>) {
54+
return eventHandler(async (event: H3Event) => {
55+
const runtimeConfig = useRuntimeConfig(event).oauth?.heroku
56+
const baseURL = 'https://id.heroku.com'
57+
config = defu(config, runtimeConfig, {
58+
authorizationURL: `${baseURL}/oauth/authorize`,
59+
tokenURL: `${baseURL}/oauth/token`,
60+
authorizationParams: {},
61+
}) as OAuthHerokuConfig
62+
63+
const query = getQuery<{ code?: string, state?: string, error?: string }>(event)
64+
65+
if (query.error) {
66+
const error = createError({
67+
statusCode: 401,
68+
message: `Heroku login failed: ${query.error || 'Unknown error'}`,
69+
data: query,
70+
})
71+
if (!onError) throw error
72+
return onError(event, error)
73+
}
74+
75+
if (!config.clientId || !config.clientSecret) {
76+
return handleMissingConfiguration(event, 'heroku', ['clientId', 'clientSecret'], onError)
77+
}
78+
79+
const redirectURL = config.redirectURL || getOAuthRedirectURL(event)
80+
const state = await handleState(event)
81+
82+
if (!query.code) {
83+
config.scope = config.scope || ['identity']
84+
return sendRedirect(
85+
event,
86+
withQuery(config.authorizationURL as string, {
87+
response_type: 'code',
88+
client_id: config.clientId,
89+
redirect_uri: redirectURL,
90+
scope: config.scope.join(' '),
91+
state,
92+
...config.authorizationParams,
93+
}),
94+
)
95+
}
96+
97+
if (query.state !== state) {
98+
handleInvalidState(event, 'heroku', onError)
99+
}
100+
101+
const tokens = await requestAccessToken(config.tokenURL as string, {
102+
body: {
103+
grant_type: 'authorization_code',
104+
client_id: config.clientId,
105+
client_secret: config.clientSecret,
106+
redirect_uri: redirectURL,
107+
code: query.code,
108+
},
109+
})
110+
111+
if (tokens.error) {
112+
return handleAccessTokenErrorResponse(event, 'heroku', tokens, onError)
113+
}
114+
115+
const accessToken = tokens.access_token
116+
const user = await $fetch(`https://api.heroku.com/account`, {
117+
headers: {
118+
Accept: 'application/vnd.heroku+json; version=3',
119+
Authorization: `Bearer ${accessToken}`,
120+
},
121+
})
122+
123+
return onSuccess(event, {
124+
user,
125+
tokens,
126+
})
127+
})
128+
}

0 commit comments

Comments
 (0)