Skip to content

Commit 3f2519d

Browse files
committed
feat(provider): ✨ Added Microsoft provider
1 parent b9d185d commit 3f2519d

File tree

12 files changed

+147
-30
lines changed

12 files changed

+147
-30
lines changed

playground/composables/useProviders.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@
22

33
export function useProviders(currentProvider: string) {
44
const providers = ref([
5-
{
6-
label: 'Microsoft Entra ID',
7-
name: 'entra',
8-
disabled: Boolean(currentProvider === 'entra'),
9-
icon: 'i-simple-icons-microsoftazure',
10-
},
115
{
126
label: 'Auth0',
137
name: 'auth0',
148
disabled: Boolean(currentProvider === 'auth0'),
159
icon: 'i-simple-icons-auth0',
1610
},
11+
{
12+
label: 'AWS Cognito',
13+
name: 'cognito',
14+
disabled: Boolean(currentProvider === 'cognito'),
15+
icon: 'i-simple-icons-amazoncognito',
16+
},
1717
{
1818
label: 'GitHub',
1919
name: 'github',
@@ -27,10 +27,16 @@ export function useProviders(currentProvider: string) {
2727
icon: 'i-simple-icons-cncf',
2828
},
2929
{
30-
label: 'AWS Cognito',
31-
name: 'cognito',
32-
disabled: Boolean(currentProvider === 'cognito'),
33-
icon: 'i-simple-icons-amazoncognito',
30+
label: 'Microsoft',
31+
name: 'microsoft',
32+
disabled: Boolean(currentProvider === 'entra'),
33+
icon: 'i-simple-icons-microsoft',
34+
},
35+
{
36+
label: 'Microsoft Entra ID',
37+
name: 'entra',
38+
disabled: Boolean(currentProvider === 'entra'),
39+
icon: 'i-simple-icons-microsoftazure',
3440
},
3541
{
3642
label: 'Zitadel',

playground/nuxt.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ export default defineNuxtConfig({
9393
userInfoUrl: 'https://api-m.sandbox.paypal.com/v1/identity/openidconnect/userinfo?schema=openid',
9494
redirectUri: 'http://127.0.0.1:3000/auth/paypal/callback',
9595
},
96+
microsoft: {
97+
clientId: '',
98+
clientSecret: '',
99+
redirectUri: 'http://localhost:3000/auth/microsoft/callback',
100+
},
96101
},
97102
session: {
98103
expirationCheck: true,

playground/pages/index.vue

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@
22
const { loggedIn, user, refresh, fetch, login, logout, currentProvider, clear } = useOidcAuth()
33
const { providers } = useProviders(currentProvider.value as string)
44
const refreshing = ref(false)
5+
const clearing = ref(false)
56
async function handleRefresh() {
67
refreshing.value = true
78
await refresh()
89
refreshing.value = false
910
}
11+
async function handleClear() {
12+
clearing.value = true
13+
await clear()
14+
clearing.value = false
15+
}
1016
</script>
1117

1218
<template>
@@ -65,8 +71,8 @@ async function handleRefresh() {
6571
</button>
6672
<button
6773
class="btn-base btn-login"
68-
:disabled="!loggedIn"
69-
@click="clear()"
74+
:disabled="!loggedIn || clearing"
75+
@click="handleClear()"
7076
>
7177
<span class="i-majesticons-delete-bin-line" />
7278
<span class="pl-2">Clear session</span>

src/module.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,15 +159,23 @@ export default defineNuxtModule<ModuleOptions>({
159159

160160
// Per provider tasks
161161
providers.forEach((provider) => {
162-
const baseUrl = process.env[`NUXT_OIDC_PROVIDERS_${provider.toUpperCase()}_BASE_URL`] || (options.providers as ProviderConfigs)[provider].baseUrl
162+
const baseUrl = process.env[`NUXT_OIDC_PROVIDERS_${provider.toUpperCase()}_BASE_URL`] || (options.providers as ProviderConfigs)[provider].baseUrl || providerPresets[provider].baseUrl
163163

164164
// Generate provider routes
165165
if (baseUrl) {
166-
(options.providers[provider] as OidcProviderConfig).authorizationUrl = generateProviderUrl(baseUrl as string, providerPresets[provider].authorizationUrl);
167-
(options.providers[provider] as OidcProviderConfig).tokenUrl = generateProviderUrl(baseUrl as string, providerPresets[provider].tokenUrl);
168-
(options.providers[provider] as OidcProviderConfig).userInfoUrl = generateProviderUrl(baseUrl as string, providerPresets[provider].userInfoUrl)
166+
let _baseUrl = baseUrl
167+
const placeholders = baseUrl.matchAll(/\{(.*?)\}/g)
168+
for (const placeholderMatch of placeholders) {
169+
if (placeholderMatch && options.providers[provider] && Object.prototype.hasOwnProperty.call(options.providers[provider], placeholderMatch[1])) {
170+
_baseUrl = _baseUrl.replace(`{${placeholderMatch[1]}}`, (options.providers[provider] as any)[placeholderMatch[1]])
171+
}
172+
}
173+
(options.providers[provider] as OidcProviderConfig).authorizationUrl = generateProviderUrl(_baseUrl as string, providerPresets[provider].authorizationUrl);
174+
(options.providers[provider] as OidcProviderConfig).tokenUrl = generateProviderUrl(_baseUrl as string, providerPresets[provider].tokenUrl)
175+
if (providerPresets[provider].userInfoUrl && !providerPresets[provider].userInfoUrl.startsWith('https'))
176+
(options.providers[provider] as OidcProviderConfig).userInfoUrl = generateProviderUrl(_baseUrl as string, providerPresets[provider].userInfoUrl)
169177
if (providerPresets[provider].logoutUrl)
170-
(options.providers[provider] as OidcProviderConfig).logoutUrl = generateProviderUrl(baseUrl as string, providerPresets[provider].logoutUrl)
178+
(options.providers[provider] as OidcProviderConfig).logoutUrl = generateProviderUrl(_baseUrl as string, providerPresets[provider].logoutUrl)
171179
}
172180

173181
// Replace placeholder parameters from provider presets

src/runtime/providers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export { cognito } from './cognito.js'
44
export { entra } from './entra.js'
55
export { github } from './github.js'
66
export { keycloak } from './keycloak.js'
7+
export { microsoft } from './microsoft.js'
78
export { oidc } from './oidc.js'
89
export { paypal } from './paypal.js'
910
export { zitadel } from './zitadel.js'

src/runtime/providers/microsoft.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { ofetch } from 'ofetch'
2+
import { defineOidcProvider } from '../server/utils/provider'
3+
4+
type MicrosoftRequiredFields = 'clientId' | 'clientSecret'
5+
6+
interface MicrosoftAdditionalFields {
7+
/**
8+
* Optional. Indicates the type of user interaction that is required. Valid values are `login`, `none`, `consent`, and `select_account`.
9+
* @default 'login'
10+
*/
11+
prompt?: 'login' | 'none' | 'consent' | 'select_account'
12+
/**
13+
* Optional. You can use this parameter to pre-fill the username and email address field of the sign-in page for the user. Apps can use this parameter during reauthentication, after already extracting the login_hint optional claim from an earlier sign-in.
14+
* @default undefined
15+
*/
16+
loginHint?: string
17+
/**
18+
* Optional. Enables sign-out to occur without prompting the user to select an account. To use logout_hint, enable the login_hint optional claim in your client application and use the value of the login_hint optional claim as the logout_hint parameter.
19+
* @default undefined
20+
*/
21+
logoutHint?: string
22+
/**
23+
* Optional. If included, the app skips the email-based discovery process that user goes through on the sign-in page, leading to a slightly more streamlined user experience.
24+
* @default undefined
25+
*/
26+
domainHint?: boolean
27+
}
28+
29+
interface MicrosoftProviderConfig {
30+
/**
31+
* Required. The tenant id is used to automatically configure the correct endpoint urls for the Microsoft provider to work.
32+
* @default 'login'
33+
*/
34+
tenantId: 'login' | 'none' | 'consent' | 'select_account'
35+
}
36+
37+
export const microsoft = defineOidcProvider<MicrosoftAdditionalFields, MicrosoftRequiredFields, MicrosoftProviderConfig>({
38+
tokenRequestType: 'form-urlencoded',
39+
logoutRedirectParameterName: 'post_logout_redirect_uri',
40+
grantType: 'authorization_code',
41+
// scopeInTokenRequest: true,
42+
scope: ['openid', 'User.Read'],
43+
pkce: true,
44+
state: true,
45+
nonce: true,
46+
requiredProperties: [
47+
'clientId',
48+
'clientSecret',
49+
'authorizationUrl',
50+
'tokenUrl',
51+
'redirectUri',
52+
],
53+
responseType: 'code id_token',
54+
async openIdConfiguration(config: any) {
55+
const openIdConfig = await ofetch(`https://login.microsoftonline.com/${config.tenantId ? config.tenantId : 'common'}/v2.0/.well-known/openid-configuration`)
56+
openIdConfig.issuer = config.tenantId ? [`https://login.microsoftonline.com/${config.tenantId}/v2.0`, openIdConfig.issuer] : undefined
57+
return openIdConfig
58+
},
59+
sessionConfiguration: {
60+
expirationCheck: true,
61+
automaticRefresh: true,
62+
expirationThreshold: 1800,
63+
},
64+
skipAccessTokenParsing: true,
65+
validateAccessToken: false,
66+
validateIdToken: true,
67+
additionalAuthParameters: {
68+
prompt: 'select_account',
69+
},
70+
optionalClaims: ['name', 'preferred_username'],
71+
baseUrl: 'https://login.microsoftonline.com/common',
72+
authorizationUrl: '/oauth2/v2.0/authorize',
73+
tokenUrl: '/oauth2/v2.0/token',
74+
userInfoUrl: 'https://graph.microsoft.com/v1.0/me', // https://graph.microsoft.com/oidc/userinfo"
75+
})

src/runtime/server/handler/callback.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { normalizeURL, parseURL } from 'ufo'
77
import * as providerPresets from '../../providers'
88
import { validateConfig } from '../utils/config'
99
import { configMerger, convertObjectToSnakeCase, convertTokenRequestToType, oidcErrorHandler, useOidcLogger } from '../utils/oidc'
10-
import { encryptToken, parseJwtToken, validateToken } from '../utils/security'
10+
import { encryptToken, type JwtPayload, parseJwtToken, validateToken } from '../utils/security'
1111
import { getUserSessionId, setUserSession, useAuthSession } from '../utils/session'
1212
// @ts-expect-error - Missing Nitro type exports in Nuxt
1313
import { useRuntimeConfig, useStorage } from '#imports'
@@ -113,12 +113,21 @@ function callbackEventHandler({ onSuccess }: OAuthConfig<UserSession>) {
113113
let tokens: Tokens
114114

115115
// Validate tokens only if audience is matched
116-
const accessToken = parseJwtToken(tokenResponse.access_token, !!config.skipAccessTokenParsing)
117-
const idToken = tokenResponse.id_token ? parseJwtToken(tokenResponse.id_token) : undefined
116+
let accessToken: JwtPayload | Record<string, never>
117+
let idToken: JwtPayload | Record<string, never> | undefined
118+
if (!tokenResponse.access_token)
119+
return oidcErrorHandler(event, `[${provider}] No access token found`)
120+
try {
121+
accessToken = parseJwtToken(tokenResponse.access_token, !!config.skipAccessTokenParsing)
122+
idToken = tokenResponse.id_token ? parseJwtToken(tokenResponse.id_token) : undefined
123+
}
124+
catch (error) {
125+
return oidcErrorHandler(event, `[${provider}] Token parsing failed: ${error}`)
126+
}
118127
if ([config.audience as string, config.clientId].some(audience => accessToken.aud?.includes(audience) || idToken?.aud?.includes(audience)) && (config.validateAccessToken || config.validateIdToken)) {
119128
// Get OIDC configuration
120129
const openIdConfiguration = (config.openIdConfiguration && typeof config.openIdConfiguration === 'object') ? config.openIdConfiguration : typeof config.openIdConfiguration === 'string' ? await ofetch(config.openIdConfiguration) : await (config.openIdConfiguration!)(config)
121-
const validationOptions = { jwksUri: openIdConfiguration.jwks_uri as string, issuer: openIdConfiguration.issuer as string, ...config.audience && { audience: [config.audience, config.clientId] } }
130+
const validationOptions = { jwksUri: openIdConfiguration.jwks_uri as string, ...openIdConfiguration.issuer && { issuer: openIdConfiguration.issuer as string }, ...config.audience && { audience: [config.audience, config.clientId] } }
122131
try {
123132
tokens = {
124133
accessToken: config.validateAccessToken ? await validateToken(tokenResponse.access_token, validationOptions) : accessToken,

src/runtime/server/handler/login.get.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ function loginEventHandler() {
2929
state: generateRandomUrlSafeString(),
3030
codeVerifier: generatePkceVerifier(),
3131
redirect: getRequestHeader(event, 'referer'),
32+
nonce: undefined,
3233
})
3334

3435
// Get client side query parameters

src/runtime/server/utils/oidc.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,9 @@ export function convertObjectToSnakeCase(object: Record<string, any>) {
129129
}, {} as Record<string, any>)
130130
}
131131

132-
export function oidcErrorHandler(event: H3Event, errorText: string, errorCode: number = 500) {
132+
export async function oidcErrorHandler(event: H3Event, errorText: string, errorCode: number = 500) {
133133
const logger = useOidcLogger()
134-
clearUserSession(event, true)
134+
await clearUserSession(event, true)
135135
logger.error(errorText, 'code:', errorCode)
136136
return sendRedirect(
137137
event,

src/runtime/server/utils/provider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,8 @@ const configMerger = createDefu((obj, key, value) => {
203203
}
204204
})
205205

206-
export function defineOidcProvider<TConfig, TRequired extends keyof OidcProviderConfig>(config: Partial<OidcProviderConfig> & { additionalAuthParameters?: Partial<TConfig>; additionalTokenParameters?: Partial<TConfig>; additionalLogoutParameters?: Partial<TConfig> } = {} as any) {
207-
const defaults: Partial<OidcProviderConfig> = {
206+
export function defineOidcProvider<TConfig, TRequired extends keyof (OidcProviderConfig & TProviderConfig), TProviderConfig extends object = object>(config: Partial<Omit<OidcProviderConfig, 'requiredProperties'> & { requiredProperties?: (keyof (TProviderConfig & OidcProviderConfig))[] }> & Partial<TProviderConfig> & { additionalAuthParameters?: Partial<TConfig>; additionalTokenParameters?: Partial<TConfig>; additionalLogoutParameters?: Partial<TConfig> } = {} as object) {
207+
const defaults: Partial<Omit<OidcProviderConfig, 'requiredProperties'> & { requiredProperties?: (keyof (TProviderConfig & OidcProviderConfig))[] }> = {
208208
clientId: '',
209209
redirectUri: '',
210210
clientSecret: '',

0 commit comments

Comments
 (0)