Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
"@nuxt/kit": "^3.11.2",
"defu": "^6.1.4",
"hookable": "^5.5.3",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.1.0",
"ofetch": "^1.3.4",
"ohash": "^1.1.3",
"pathe": "^1.1.2",
Expand All @@ -55,4 +57,4 @@
"vitest": "^1.5.0",
"vue-tsc": "^2.0.13"
}
}
}
103 changes: 88 additions & 15 deletions src/runtime/server/lib/oauth/microsoft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from
import { withQuery, parsePath } from 'ufo'
import { ofetch } from 'ofetch'
import { defu } from 'defu'
import jwt from 'jsonwebtoken'
import jwksClient from 'jwks-rsa'
import { useRuntimeConfig } from '#imports'

export interface OAuthMicrosoftConfig {
Expand Down Expand Up @@ -45,6 +47,13 @@ export interface OAuthMicrosoftConfig {
* @see https://docs.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http
*/
userURL?: string
/**
* Flag to call the "me" endpoint. May not be callable depending on scopes used.
* If not used, Name and Email will be parsed from the returned JWT token.
* @default false
* @see https://learn.microsoft.com/en-us/entra/identity-platform/access-tokens
*/
useUser?: boolean
/**
* Extra authorization parameters to provide to the authorization URL
* @see https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
Expand All @@ -69,7 +78,9 @@ export function microsoftEventHandler({ config, onSuccess, onError }: OAuthConfi
return eventHandler(async (event: H3Event) => {
config = defu(config, useRuntimeConfig(event).oauth?.microsoft, {
authorizationParams: {},
useUser: false,
}) as OAuthMicrosoftConfig

const { code } = getQuery(event)

if (!config.clientId || !config.clientSecret || !config.tenant) {
Expand Down Expand Up @@ -133,24 +144,86 @@ export function microsoftEventHandler({ config, onSuccess, onError }: OAuthConfi

const tokenType = tokens.token_type
const accessToken = tokens.access_token
const userURL = config.userURL || 'https://graph.microsoft.com/v1.0/me'

// TODO: improve typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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,
let user: any = {}

if (config.useUser) {
const userURL = config.userURL || 'https://graph.microsoft.com/v1.0/me'
user = 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)
}
}
else {
// required to unsafely decode to get the Kid from the header
const decoded = jwt.decode(accessToken, { complete: true })
if (!decoded) {
const error = createError({
statusCode: 401,
message: `Microsoft login failed: ${user.error || 'Failed to decoded JWT'}`,
})
if (!onError) throw error
return onError(event, error)
}

const kid = decoded.header.kid
if (!kid) {
const error = createError({
statusCode: 401,
message: `Microsoft login failed: ${user.error || 'Missing Kid'}`,
})
if (!onError) throw error
return onError(event, error)
}

const client = jwksClient({
jwksUri: 'https://login.microsoftonline.com/common/discovery/keys',
})

// use kid to validate signature and get signingKey
const key = await client.getSigningKey(kid)
const signingKey = key.getPublicKey()

// eslint-disable-next-line @typescript-eslint/no-explicit-any
jwt.verify(accessToken, signingKey, function (err: any, decoded: any) {
if (decoded) {
const msJwtVersion: '1.0' | '2.0' = decoded.ver

if (msJwtVersion === '2.0') {
user.displayName = decoded.name
user.mail = decoded.preferred_username
}
else {
const firstName = decoded.given_name
const lastName = decoded.family_name
user.displayName = `${firstName} ${lastName}`
user.mail = decoded.unique_name
}
}
else {
const error = createError({
statusCode: 401,
message: `Microsoft login failed: ${user.error || 'Token verification failed'}`,
data: err,
})
if (!onError) throw error
return onError(event, error)
}
})
if (!onError) throw error
return onError(event, error)
}

return onSuccess(event, {
Expand Down