Skip to content

feat: add Bitrix24 OAuth provider #409

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ It can also be set using environment variables:
- AWS Cognito
- Azure B2C
- Battle.net
- Bitrix24
- Bluesky (AT Protocol)
- Discord
- Dropbox
Expand Down
5 changes: 4 additions & 1 deletion playground/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,7 @@ NUXT_OAUTH_SLACK_REDIRECT_URL=
#Heroku
NUXT_OAUTH_HEROKU_CLIENT_ID=
NUXT_OAUTH_HEROKU_CLIENT_SECRET=
NUXT_OAUTH_HEROKU_REDIRECT_URL=
NUXT_OAUTH_HEROKU_REDIRECT_URL=
# Bitrix24
NUXT_OAUTH_BITRIX24_CLIENT_ID=
NUXT_OAUTH_BITRIX24_CLIENT_SECRET=
28 changes: 28 additions & 0 deletions playground/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,34 @@ const providers = computed(() =>
disabled: Boolean(user.value?.heroku),
icon: 'i-simple-icons-heroku',
},
{
label: user.value?.bitrix24?.name || 'Bitrix24',
avatar: {
size: user.value?.bitrix24?.photo ? '2xs' : 'xs',
ui: {

},
src: user.value?.bitrix24?.photo || 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MiA0MiIgZmlsbD0iY3VycmVudENvbG9yIiBjbGFzcz0ic2l6ZS0yNCB0ZXh0LWJhc2UtbWFzdGVyIGRhcms6dGV4dC1iYXNlLTIwMCI+PHBhdGggZmlsbD0iY3VycmVudENvbG9yIiBkPSJNMjIuMDkgMTcuOTI2aC0xLjM4NnYzLjcxNmgzLjU1MXYtMS4zODZIMjIuMDl6bS0uNjE2IDcuMzU2YTQuNzE4IDQuNzE4IDAgMSAxIDAtOS40MzYgNC43MTggNC43MTggMCAwIDEgMCA5LjQzNm05LjE5NS02QTUuMTkgNS4xOSAwIDAgMCAyMy43MjEgMTRhNS4xOSA1LjE5IDAgMCAwLTkuODcyIDEuNjlBNi4yMzQgNi4yMzQgMCAwIDAgMTUuMjMzIDI4aDE0Ljc2MWMyLjQ0NCAwIDQuNDI1LTEuNzI0IDQuNDI1LTQuNDI1IDAtMy40OTctMy40MDYtNC4zNzktMy43NS00LjI5MyIvPjwvc3ZnPg==',
},
click() {
// open user profile ////
if (user.value?.bitrix24) {
window.open(`${user.value?.bitrix24?.targetOrigin}/company/personal/user/${user.value?.bitrix24?.id}/`)
return
}

// make auth ////
const authorizationServer = prompt('Enter your Bitrix24 URL', '')
if (authorizationServer) {
navigateTo({
path: '/auth/bitrix24',
query: { authorizationServer },
}, {
external: true,
})
}
},
},
].map(p => ({
...p,
prefetch: false,
Expand Down
6 changes: 6 additions & 0 deletions playground/auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ declare module '#auth-utils' {
salesforce?: string
slack?: string
heroku?: string
bitrix24?: {
id: number
name: string
photo: string
targetOrigin: string
}
}

interface UserSession {
Expand Down
25 changes: 25 additions & 0 deletions playground/server/routes/auth/bitrix24.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export default defineOAuthBitrix24EventHandler({
config: {},
async onSuccess(event, { user, payload }) {
const userToSet = user?.name?.firstName && user?.name?.lastName
? `${user.name.firstName} ${user.name.lastName}`
: user?.name?.firstName || user?.name?.lastName || user?.id || payload.memberId

await setUserSession(event, {
user: {
bitrix24: {
id: user.id,
name: userToSet,
photo: user.photo,
targetOrigin: user.targetOrigin,
},
},
secure: {
b24Tokens: payload,
},
loggedInAt: Date.now(),
})

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

export interface Bitrix24Tokens {
accessToken: string
clientEndpoint: string
domain: string
expiresIn: number
memberId: string
refreshToken: string
scope: string
serverEndpoint: string
status: string
}

export interface Bitrix24UserProfile {
id?: number
isAdmin?: boolean
/**
* account address BX24 ( https://name.bitrix24.com )
*/
targetOrigin?: string
name?: {
firstName?: string
lastName?: string
}
gender?: string
photo?: string
timeZone?: string
timeZoneOffset?: number
}

/**
* Bitrix24
* @memo Not send: `scope`, `redirect_uri`
*
* @see https://apidocs.bitrix24.com/api-reference/oauth/index.html
*/
export interface OAuthBitrix24Config {
/**
* Bitrix24 OAuth Client ID
* @default process.env.NUXT_OAUTH_BITRIX24_CLIENT_ID
*/
clientId?: string

/**
* Bitrix24 OAuth Client Secret
* @default process.env.NUXT_OAUTH_BITRIX24_CLIENT_SECRET
*/
clientSecret?: string

/**
* Bitrix24 OAuth Authorization URL
* @default '${baseURL}/oauth/authorize/'
*/
authorizationURL?: string

/**
* Bitrix24 OAuth Token URL
* @default 'https://oauth.bitrix.info/oauth/token/'
*/
tokenURL?: string
}

export function defineOAuthBitrix24EventHandler({
config,
onSuccess,
onError,
}: OAuthConfig<OAuthBitrix24Config, {
user: Bitrix24UserProfile
payload: Bitrix24Tokens
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tokens: any
}>) {
return eventHandler(async (event: H3Event) => {
if (event.method === 'HEAD') {
event.node.res.end()
return
}

const runtimeConfig = useRuntimeConfig(event).oauth?.bitrix24

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

const authorizationServer = query?.authorizationServer
if (
!query.code
&& typeof authorizationServer === 'undefined'
) {
const error = createError({
statusCode: 500,
message: 'Query parameter `authorizationServer` empty or missing. Please provide a valid Bitrix24 authorizationServer.',
})
if (!onError) throw error
return onError(event, error)
}

config = defu(config, runtimeConfig, {
authorizationURL: `${authorizationServer}/oauth/authorize/`,
tokenURL: `https://oauth.bitrix.info/oauth/token/`,
}) as OAuthBitrix24Config

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

const state = await handleState(event)

if (!query.code) {
// Redirect to Bitrix24 oAuth page
return sendRedirect(
event,
withQuery(config.authorizationURL as string, {
client_id: config.clientId,
state,
}),
)
}

if (query?.state !== state) {
handleInvalidState(event, 'bitrix24', onError)
}

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

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

const payload: Bitrix24Tokens = {
accessToken: tokens.access_token,
clientEndpoint: tokens.client_endpoint,
domain: tokens.domain,
expiresIn: tokens.expires_in,
memberId: tokens.member_id,
refreshToken: tokens.refresh_token,
scope: tokens.scope,
serverEndpoint: tokens.server_endpoint,
status: tokens.status,
}

const response = await $fetch<{
result: {
ID: string
NAME: string
ADMIN: boolean
LAST_NAME: string
PERSONAL_GENDER: string
PERSONAL_PHOTO: string
TIME_ZONE: string
TIME_ZONE_OFFSET: number
}
time: {
start: number
finish: number
duration: number
processing: number
date_start: string
date_finish: string
operating: number
}
}>(`${payload.clientEndpoint}profile` as string, {
params: {
auth: payload.accessToken,
},
})

const user = {
id: Number.parseInt(response.result.ID),
isAdmin: response.result.ADMIN,
targetOrigin: `https://${tokens.client_endpoint.replaceAll('https://', '').replaceAll('http://', '').replace(/:(80|443)$/, '').replace('/rest/', '')}`,
name: {
firstName: response.result.NAME,
lastName: response.result.LAST_NAME,
},
gender: response.result.PERSONAL_GENDER,
photo: response.result.PERSONAL_PHOTO,
timeZone: response.result.TIME_ZONE,
timeZoneOffset: response.result.TIME_ZONE_OFFSET,
}

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

export type ATProtoProvider = 'bluesky'

export type OAuthProvider = ATProtoProvider | 'atlassian' | 'auth0' | 'authentik' | 'azureb2c' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'gitea' | 'github' | 'gitlab' | 'google' | 'hubspot' | 'instagram' | 'kick' | 'keycloak' | 'line' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'seznam' | 'steam' | 'strava' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | 'apple' | 'livechat' | 'salesforce' | 'slack' | 'heroku' | (string & {})
export type OAuthProvider = ATProtoProvider | 'atlassian' | 'auth0' | 'authentik' | 'azureb2c' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'gitea' | 'github' | 'gitlab' | 'google' | 'hubspot' | 'instagram' | 'kick' | 'keycloak' | 'line' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'seznam' | 'steam' | 'strava' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | 'apple' | 'livechat' | 'salesforce' | 'slack' | 'heroku' | 'bitrix24' | (string & {})

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

Expand Down