Skip to content

Commit

Permalink
feat: added keycloak as oauth provider
Browse files Browse the repository at this point in the history
  • Loading branch information
kingyue737 committed Nov 27, 2023
1 parent 79f7ce7 commit 521e69b
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ It can also be set using environment variables:
- Spotify
- Twitch
- Battle.net
- Keycloak

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

Expand Down
5 changes: 5 additions & 0 deletions playground/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ 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=
6 changes: 6 additions & 0 deletions playground/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ const providers = computed(() => [
disabled: Boolean(user.value?.battledotnet),
icon: 'i-simple-icons-battledotnet',
},
{
label: user.value?.keycloak?.username || 'Keycloak',
to: '/auth/keycloak',
disabled: Boolean(user.value?.keycloak),
icon: 'i-simple-icons-redhat'
},
].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 @@ -8,6 +8,7 @@ declare module '#auth-utils' {
auth0?: any
discord?: any
battledotnet?: any
keycloak?: any
}
loggedInAt: number
}
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, '/')
},
})
7 changes: 7 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,12 @@ export default defineNuxtModule<ModuleOptions>({
clientId: '',
clientSecret: ''
})
// Keycloak OAuth
runtimeConfig.oauth.keycloak = defu(runtimeConfig.oauth.keycloak, {
clientId: '',
clientSecret: '',
serverUrl: '',
realm: ''
})
}
})
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,
})
})
}
4 changes: 3 additions & 1 deletion src/runtime/server/utils/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { twitchEventHandler } from '../lib/oauth/twitch'
import { auth0EventHandler } from '../lib/oauth/auth0'
import { discordEventHandler } from '../lib/oauth/discord'
import { battledotnetEventHandler } from '../lib/oauth/battledotnet'
import { keycloakEventHandler } from '../lib/oauth/keycloak'

export const oauth = {
githubEventHandler,
Expand All @@ -13,5 +14,6 @@ export const oauth = {
twitchEventHandler,
auth0EventHandler,
discordEventHandler,
battledotnetEventHandler
battledotnetEventHandler,
keycloakEventHandler
}

0 comments on commit 521e69b

Please sign in to comment.