Skip to content

Commit

Permalink
feat: added keycloak as oauth provider (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
kingyue737 authored Jan 5, 2024
1 parent 8a9747e commit 10398a6
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ It can also be set using environment variables:
- Discord
- GitHub
- Google
- Keycloak
- LinkedIn
- Microsoft
- Spotify
Expand Down
5 changes: 5 additions & 0 deletions playground/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ NUXT_OAUTH_DISCORD_CLIENT_SECRET=
# Battle.net OAuth
NUXT_OAUTH_BATTLEDOTNET_CLIENT_ID=
NUXT_OAUTH_BATTLEDOTNET_CLIENT_SECRET=
# Keycloak OAuth
NUXT_OAUTH_KEYCLOAK_CLIENT_ID=
NUXT_OAUTH_KEYCLOAK_CLIENT_SECRET=
NUXT_OAUTH_KEYCLOAK_SERVER_URL=
NUXT_OAUTH_KEYCLOAK_REALM=
# LinkedIn
NUXT_OAUTH_LINKEDIN_CLIENT_ID=
NUXT_OAUTH_LINKEDIN_CLIENT_SECRET=
7 changes: 6 additions & 1 deletion playground/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,18 @@ const providers = computed(() => [
disabled: Boolean(user.value?.microsoft),
icon: 'i-simple-icons-microsoft',
},
{
label: user.value?.keycloak?.preferred_username || 'Keycloak',
to: '/auth/keycloak',
disabled: Boolean(user.value?.keycloak),
icon: 'i-simple-icons-redhat'
},
{
label: user.value?.linkedin?.email || 'LinkedIn',
to: '/auth/linkedin',
disabled: Boolean(user.value?.linkedin),
icon: 'i-simple-icons-linkedin',
}
].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 @@ -9,6 +9,7 @@ declare module '#auth-utils' {
microsoft?: any;
discord?: any
battledotnet?: any
keycloak?: any
linkedin?: any
}
extended?: any
Expand Down
12 changes: 12 additions & 0 deletions playground/server/routes/auth/keycloak.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default oauth.keycloakEventHandler({
async onSuccess(event, { user }) {
await setUserSession(event, {
user: {
keycloak: user,
},
loggedInAt: Date.now(),
})

return sendRedirect(event, '/')
},
})
9 changes: 8 additions & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,17 @@ export default defineNuxtModule<ModuleOptions>({
clientId: '',
clientSecret: ''
})
// Keycloak OAuth
runtimeConfig.oauth.keycloak = defu(runtimeConfig.oauth.keycloak, {
clientId: '',
clientSecret: '',
serverUrl: '',
realm: ''
})
// LinkedIn OAuth
runtimeConfig.oauth.linkedin = defu(runtimeConfig.oauth.linkedin, {
clientId: '',
clientSecret: '',
clientSecret: ''
})
}
})
167 changes: 167 additions & 0 deletions src/runtime/server/lib/oauth/keycloak.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import type { H3Event } from 'h3'
import {
eventHandler,
createError,
getQuery,
getRequestURL,
sendRedirect,
} from 'h3'
import { ofetch } from 'ofetch'
import { withQuery, parsePath } from 'ufo'
import { defu } from 'defu'
import { useRuntimeConfig } from '#imports'
import type { OAuthConfig } from '#auth-utils'

export interface OAuthKeycloakConfig {
/**
* Keycloak OAuth Client ID
* @default process.env.NUXT_OAUTH_KEYCLOAK_CLIENT_ID
*/
clientId?: string
/**
* Keycloak OAuth Client Secret
* @default process.env.NUXT_OAUTH_KEYCLOAK_CLIENT_SECRET
*/
clientSecret?: string
/**
* Keycloak OAuth Server URL
* @example http://192.168.1.10:8080/auth
* @default process.env.NUXT_OAUTH_KEYCLOAK_SERVER_URL
*/
serverUrl?: string
/**
* Keycloak OAuth Realm
* @default process.env.NUXT_OAUTH_KEYCLOAK_REALM
*/
realm?: string
/**
* Keycloak OAuth Scope
* @default []
* @see https://www.keycloak.org/docs/latest/authorization_services/
* @example ['openid']
*/
scope?: string[]
}

export function keycloakEventHandler({
config,
onSuccess,
onError,
}: OAuthConfig<OAuthKeycloakConfig>) {
return eventHandler(async (event: H3Event) => {
config = defu(
config,
// @ts-ignore
useRuntimeConfig(event).oauth?.keycloak
) as OAuthKeycloakConfig

const query = getQuery(event)
const { code } = query

if (query.error) {
const error = createError({
statusCode: 401,
message: `Keycloak login failed: ${query.error || 'Unknown error'}`,
data: query,
})
if (!onError) throw error
return onError(event, error)
}

if (
!config.clientId ||
!config.clientSecret ||
!config.serverUrl ||
!config.realm
) {
const error = createError({
statusCode: 500,
message:
'Missing NUXT_OAUTH_KEYCLOAK_CLIENT_ID or NUXT_OAUTH_KEYCLOAK_CLIENT_SECRET or NUXT_OAUTH_KEYCLOAK_SERVER_URL or NUXT_OAUTH_KEYCLOAK_REALM env variables.',
})
if (!onError) throw error
return onError(event, error)
}

const realmURL = `${config.serverUrl}/realms/${config.realm}`

const authorizationURL = `${realmURL}/protocol/openid-connect/auth`
const tokenURL = `${realmURL}/protocol/openid-connect/token`
const redirectUrl = getRequestURL(event).href

if (!code) {
config.scope = config.scope || ['openid']

// Redirect to Keycloak Oauth page
return sendRedirect(
event,
withQuery(authorizationURL, {
client_id: config.clientId,
redirect_uri: redirectUrl,
scope: config.scope.join(' '),
response_type: 'code',
})
)
}

config.scope = config.scope || []
if (!config.scope.includes('openid')) {
config.scope.push('openid')
}

const tokens: any = await ofetch(tokenURL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
client_id: config.clientId,
client_secret: config.clientSecret,
grant_type: 'authorization_code',
redirect_uri: parsePath(redirectUrl).pathname,
code: code as string,
}).toString(),
}).catch((error) => {
return { error }
})

if (tokens.error) {
const error = createError({
statusCode: 401,
message: `Keycloak login failed: ${
tokens.error?.data?.error_description || 'Unknown error'
}`,
data: tokens,
})
if (!onError) throw error
return onError(event, error)
}

const accessToken = tokens.access_token

const user: any = await ofetch(
`${realmURL}/protocol/openid-connect/userinfo`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
}
)

if (!user) {
const error = createError({
statusCode: 500,
message: 'Could not get Keycloak user',
data: tokens,
})
if (!onError) throw error
return onError(event, error)
}

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

export const oauth = {
Expand All @@ -17,5 +18,6 @@ export const oauth = {
microsoftEventHandler,
discordEventHandler,
battledotnetEventHandler,
keycloakEventHandler,
linkedinEventHandler,
}

0 comments on commit 10398a6

Please sign in to comment.