Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added Microsoft as oauth provider #8

Merged
merged 11 commits into from
Nov 29, 2023
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,13 @@ It can also be set using environment variables:
#### Supported OAuth Providers

- Auth0
- Battle.net
- Discord
- GitHub
- Google
- Microsoft
- Spotify
- Twitch
- Battle.net

You can add your favorite provider by creating a new file in [src/runtime/server/lib/oauth/](./src/runtime/server/lib/oauth/).

Expand Down
6 changes: 5 additions & 1 deletion playground/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ NUXT_OAUTH_TWITCH_CLIENT_SECRET=
NUXT_OAUTH_AUTH0_CLIENT_ID=
NUXT_OAUTH_AUTH0_CLIENT_SECRET=
NUXT_OAUTH_AUTH0_DOMAIN=
# Microsoft OAuth
NUXT_OAUTH_MICROSOFT_CLIENT_ID=
NUXT_OAUTH_MICROSOFT_CLIENT_SECRET=
NUXT_OAUTH_MICROSOFT_TENANT=
# Discord
NUXT_OAUTH_DISCORD_CLIENT_ID=
NUXT_OAUTH_DISCORD_CLIENT_SECRET=
# Battle.net OAuth
NUXT_OAUTH_BATTLEDOTNET_CLIENT_ID=
NUXT_OAUTH_BATTLEDOTNET_CLIENT_SECRET=
NUXT_OAUTH_BATTLEDOTNET_CLIENT_SECRET=
7 changes: 7 additions & 0 deletions playground/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ const providers = computed(() => [
disabled: Boolean(user.value?.battledotnet),
icon: 'i-simple-icons-battledotnet',
},
{
label: user.value?.microsoft?.displayName || 'Microsoft',
to: '/auth/microsoft',
disabled: Boolean(user.value?.microsoft),
icon: 'i-simple-icons-microsoft',
}

].map(p => ({
...p,
prefetch: false,
Expand Down
1 change: 1 addition & 0 deletions playground/auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ declare module '#auth-utils' {
google?: any
twitch?: any
auth0?: any
microsoft?: any;
discord?: any
battledotnet?: any
}
Expand Down
13 changes: 13 additions & 0 deletions playground/server/routes/auth/microsoft.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default oauth.microsoftEventHandler({
async onSuccess(event, { user }) {
await setUserSession(event, {
user: {
microsoft: user,
},
loggedInAt: Date.now()
})

return sendRedirect(event, '/')
}
})

10 changes: 10 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ export default defineNuxtModule<ModuleOptions>({
clientSecret: '',
domain: ''
})
// Microsoft OAuth
runtimeConfig.oauth.microsoft = defu(runtimeConfig.oauth.microsoft, {
clientId: '',
clientSecret: '',
tenant: '',
scope: [],
authorizationURL: '',
tokenURL: '',
userURL: ''
})
// Discord OAuth
runtimeConfig.oauth.discord = defu(runtimeConfig.oauth.discord, {
clientId: '',
Expand Down
1 change: 0 additions & 1 deletion src/runtime/server/lib/oauth/discord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ export function discordEventHandler({ config, onSuccess, onError }: OAuthConfig)
return { error }
})
if (tokens.error) {
console.log(tokens)
const error = createError({
statusCode: 401,
message: `Discord login failed: ${tokens.error?.data?.error_description || 'Unknown error'}`,
Expand Down
144 changes: 144 additions & 0 deletions src/runtime/server/lib/oauth/microsoft.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import type { H3Event, H3Error } from 'h3'
import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from 'h3'
import { withQuery, parsePath } from 'ufo'
import { ofetch } from 'ofetch'
import { defu } from 'defu'
import { useRuntimeConfig } from '#imports'

export interface OAuthMicrosoftConfig {
/**
* Microsoft OAuth Client ID
* @default process.env.NUXT_OAUTH_MICROSOFT_CLIENT_ID
*/
clientId?: string
/**
* Microsoft OAuth Client Secret
* @default process.env.NUXT_OAUTH_MICROSOFT_CLIENT_SECRET
*/
clientSecret?: string
/**
* Microsoft OAuth Tenant ID
* @default process.env.NUXT_OAUTH_MICROSOFT_TENANT
*/
tenant?: string
/**
* Microsoft OAuth Scope
* @default ['User.Read']
* @see https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc
*/
scope?: string[]
/**
* Microsoft OAuth Authorization URL
* @default https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize
* @see https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
*/
authorizationURL?: string
/**
* Microsoft OAuth Token URL
* @default https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token
* @see https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
*/
tokenURL?: string
/**
* Microsoft OAuth User URL
* @default https://graph.microsoft.com/v1.0/me
* @see https://docs.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http
*/
userURL?: string
}

interface OAuthConfig {
config?: OAuthMicrosoftConfig
onSuccess: (event: H3Event, result: { user: any, tokens: any }) => Promise<void> | void
onError?: (event: H3Event, error: H3Error) => Promise<void> | void
}

export function microsoftEventHandler({ config, onSuccess, onError }: OAuthConfig) {
return eventHandler(async (event: H3Event) => {
// @ts-ignore
config = defu(config, useRuntimeConfig(event).oauth?.microsoft) as OAuthMicrosoftConfig
const { code } = getQuery(event)

if (!config.clientId || !config.clientSecret || !config.tenant) {
const error = createError({
statusCode: 500,
message: 'Missing NUXT_OAUTH_MICROSOFT_CLIENT_ID or NUXT_OAUTH_MICROSOFT_CLIENT_SECRET or NUXT_OAUTH_MICROSOFT_TENANT env variables.'
})
if (!onError) throw error
return onError(event, error)
}

const authorizationURL = config.authorizationURL || `https://login.microsoftonline.com/${config.tenant}/oauth2/v2.0/authorize`
const tokenURL = config.tokenURL || `https://login.microsoftonline.com/${config.tenant}/oauth2/v2.0/token`

const redirectUrl = getRequestURL(event).href
if (!code) {

const scope = config.scope && config.scope.length > 0 ? config.scope : ['User.Read']
// Redirect to Microsoft Oauth page
return sendRedirect(
event,
withQuery(authorizationURL as string, {
client_id: config.clientId,
response_type: 'code',
redirect_uri: redirectUrl,
scope: scope.join('%20'),
})
)
}

const data = new URLSearchParams()
data.append('grant_type', 'authorization_code')
data.append('client_id', config.clientId)
data.append('client_secret', config.clientSecret)
data.append('redirect_uri', parsePath(redirectUrl).pathname)
data.append('code', String(code))

const tokens: any = await ofetch(
tokenURL as string,
{
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: data,
}
).catch(error => {
return { error }
})
if (tokens.error) {
const error = createError({
statusCode: 401,
message: `Microsoft login failed: ${tokens.error?.data?.error_description || 'Unknown error'}`,
data: tokens
})
if (!onError) throw error
return onError(event, error)
}

const tokenType = tokens.token_type
const accessToken = tokens.access_token
const userURL = config.userURL || 'https://graph.microsoft.com/v1.0/me'
const user: any = await ofetch(userURL, {
headers: {
Authorization: `${tokenType} ${accessToken}`
}
}).catch(error => {
return { error }
})
if (user.error) {
const error = createError({
statusCode: 401,
message: `Microsoft login failed: ${user.error || 'Unknown error'}`,
data: user
})
if (!onError) throw error
return onError(event, error)
}

return onSuccess(event, {
tokens,
user
})
})
}
2 changes: 2 additions & 0 deletions src/runtime/server/utils/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { googleEventHandler } from '../lib/oauth/google'
import { spotifyEventHandler } from '../lib/oauth/spotify'
import { twitchEventHandler } from '../lib/oauth/twitch'
import { auth0EventHandler } from '../lib/oauth/auth0'
import { microsoftEventHandler} from '../lib/oauth/microsoft'
import { discordEventHandler } from '../lib/oauth/discord'
import { battledotnetEventHandler } from '../lib/oauth/battledotnet'

Expand All @@ -12,6 +13,7 @@ export const oauth = {
googleEventHandler,
twitchEventHandler,
auth0EventHandler,
microsoftEventHandler,
discordEventHandler,
battledotnetEventHandler
}