Skip to content

Commit

Permalink
feat: add seznam oauth provider (#285)
Browse files Browse the repository at this point in the history
* feat: add seznam oauth provider

* chore: add user type

---------

Co-authored-by: Sébastien Chopin <[email protected]>
  • Loading branch information
stranavad and atinux authored Nov 15, 2024
1 parent bfa2a88 commit db876cf
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ It can also be set using environment variables:
- Microsoft
- PayPal
- Polar
- Seznam
- Spotify
- Steam
- TikTok
Expand Down
3 changes: 3 additions & 0 deletions playground/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ NUXT_OAUTH_INSTAGRAM_CLIENT_SECRET=
# PayPal
NUXT_OAUTH_PAYPAL_CLIENT_ID=
NUXT_OAUTH_PAYPAL_CLIENT_SECRET=
# Seznam
NUXT_OAUTH_SEZNAM_CLIENT_ID=
NUXT_OAUTH_SEZNAM_CLIENT_SECRET=
# Steam
NUXT_OAUTH_STEAM_API_KEY=
# X
Expand Down
6 changes: 6 additions & 0 deletions playground/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ const providers = computed(() =>
disabled: Boolean(user.value?.authentik),
icon: 'i-simple-icons-authentik',
},
{
label: user.value?.seznam || 'Seznam',
to: '/auth/seznam',
disabled: Boolean(user.value?.seznam),
icon: 'i-gravity-ui-lock',
},
].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 @@ -30,6 +30,7 @@ declare module '#auth-utils' {
polar?: string
zitadel?: string
authentik?: string
seznam?: string
}

interface UserSession {
Expand Down
15 changes: 15 additions & 0 deletions playground/server/routes/auth/seznam.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default defineOAUthSeznamEventHandler({
config: {
scope: ['identity'],
},
async onSuccess(event, { user }) {
await setUserSession(event, {
user: {
seznam: `${user.firstname} ${user.lastname}`,
},
loggedInAt: Date.now(),
})

return sendRedirect(event, '/')
},
})
6 changes: 6 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,5 +335,11 @@ export default defineNuxtModule<ModuleOptions>({
domain: '',
redirectURL: '',
})
// Seznam OAuth
runtimeConfig.oauth.seznam = defu(runtimeConfig.oauth.seznam, {
clientId: '',
clientSecret: '',
redirectURL: '',
})
},
})
179 changes: 179 additions & 0 deletions src/runtime/server/lib/oauth/seznam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import defu from 'defu'
import type { H3Event } from 'h3'
import { eventHandler, getQuery, sendRedirect } from 'h3'
import { withQuery } from 'ufo'
import { getOAuthRedirectURL, handleAccessTokenErrorResponse, handleMissingConfiguration, requestAccessToken } from '../utils'
import { useRuntimeConfig } from '#imports'
import type { OAuthConfig } from '#auth-utils'

export interface OAuthSeznamConfig {
/**
* Seznam OAuth Client ID
* @default process.env.NUXT_OAUTH_SEZNAM_CLIENT_ID
*/
clientId?: string

/**
* Seznam OAuth Client Secret
* @default process.env.NUXT_OAUTH_SEZNAM_CLIENT_SECRET
*/
clientSecret?: string

/**
* Seznam OAuth Scope
* @default ['identity']
* @see https://vyvojari.seznam.cz/oauth/scopes?lang=en
* @example ['identity', 'avatar']
*/
scope?: string[]

/**
* Redirect URL to to allow overriding for situations like prod failing to determine public hostname
* Redirect URL has to be set as well in the Seznam OAuth settings https://vyvojari.seznam.cz/oauth/admin in order for it to work
* @see https://vyvojari.seznam.cz/oauth/admin
* @default process.env.NUXT_OAUTH_SEZNAM_REDIRECT_URL
*/
redirectURL?: string

/**
* Seznam OAuth Authorization URL
* @default 'https://login.szn.cz/api/v1/oauth/auth'
*/
authorizationURL?: string

/**
* Seznam OAuth Token URL
* @default 'https://login.szn.cz/api/v1/oauth/token'
*/
tokenURL?: string

/**
* Seznam OAuth User URL
* @default 'https://login.szn.cz/api/v1/user'
*/
userURL?: string
}

export interface OAuthSeznamUser {
/**
* Unique persistent user account identifier
*/
oauth_user_id: string

/**
* The user's e-mail address or null for users without an e-mail
*/
email: string | null

/**
* Given name (when available)
*/
firstname: string

/**
* Family name (when available)
*/
lastname: string

/**
* User account identifier suitable for usage within the Seznam ad infrastructure
*/
advert_user_id: string

/**
* (only when provided and validated by the user; null otherwise)
* Available only when you use the contact-phone scope
* @see https://vyvojari.seznam.cz/oauth/scopes?lang=en
*/
contact_phone?: string | null

/**
* the image's URL
* Available only when you use the avatar scope
* @see https://vyvojari.seznam.cz/oauth/scopes?lang=en
*/
avatar_url?: string | null

/**
* true/false value corresponding to the user's adult status
* Available only when you use the avatar scope
* @see https://vyvojari.seznam.cz/oauth/scopes?lang=en
*/
adulthood?: boolean

/**
* date of birth in the ISO 8601 format (only when present; null otherwise)
* Available only when you use the birthday scope
* @see https://vyvojari.seznam.cz/oauth/scopes?lang=en
*/
birthday?: string | null

/**
* one of the allowed strings "Male" / "Female" / "Other" (null if not set)
* Available only when you use the gender scope
* @see https://vyvojari.seznam.cz/oauth/scopes?lang=en
*/
gender?: string | null
}

export function defineOAUthSeznamEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthSeznamConfig, OAuthSeznamUser>) {
return eventHandler(async (event: H3Event) => {
config = defu(config, useRuntimeConfig(event).oauth?.google, {
authorizationURL: 'https://login.szn.cz/api/v1/oauth/auth',
tokenURL: 'https://login.szn.cz/api/v1/oauth/token',
userURL: 'https://login.szn.cz/api/v1/user',
}) as OAuthSeznamConfig

const query = getQuery<{ code?: string, state?: string }>(event)

if (!config.clientId) {
return handleMissingConfiguration(event, 'seznam', ['clientId'], onError)
}

const redirectURL = config.redirectURL || getOAuthRedirectURL(event)
if (!query.code) {
config.scope = config.scope || ['identity'] // identity is mandatory

// Redirect to Seznam Oauth page
return sendRedirect(
event,
withQuery(config.authorizationURL as string, {
response_type: 'code',
client_id: config.clientId,
redirect_uri: redirectURL,
scope: config.scope.join(','),
state: query.state || '',
}),
)
}

const tokens = await requestAccessToken(config.tokenURL as string, {
body: {
grant_type: 'authorization_code',
code: query.code as string,
client_id: config.clientId,
client_secret: config.clientSecret,
redirect_uri: redirectURL,
},
})

if (tokens.error) {
return handleAccessTokenErrorResponse(event, 'seznam', tokens, onError)
}

const accessToken = tokens.access_token
const user: OAuthSeznamUser = await $fetch(
config.userURL as string,
{
headers: {
Authorization: `bearer ${accessToken}`,
},
},
)

return onSuccess(event, {
tokens,
user,
})
})
}
2 changes: 1 addition & 1 deletion src/runtime/types/oauth-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { H3Event, H3Error } from 'h3'

export type OAuthProvider = 'auth0' | 'authentik' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'github' | 'gitlab' | 'google' | 'instagram' | 'keycloak' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'steam' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | (string & {})
export type OAuthProvider = 'auth0' | 'authentik' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'github' | 'gitlab' | 'google' | 'instagram' | 'keycloak' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'seznam' | 'steam' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | (string & {})

export type OnError = (event: H3Event, error: H3Error) => Promise<void> | void

Expand Down

0 comments on commit db876cf

Please sign in to comment.