UNPKG

open-collaboration-server

Version:

Open Collaboration Server implementation, part of the Open Collaboration Tools project

108 lines (87 loc) 3.97 kB
// ****************************************************************************** // Copyright 2025 TypeFox GmbH // This program and the accompanying materials are made available under the // terms of the MIT License, which is available in the project root. // ****************************************************************************** import { AuthProvider } from 'open-collaboration-protocol'; import { Strategy } from 'passport'; import { OAuthEndpoint, ThirdParty } from './oauth-endpoint.js'; import OAuth2Strategy, { VerifyCallback } from 'passport-oauth2'; import { injectable, postConstruct } from 'inversify'; @injectable() export class KeycloakOAuthEndpoint extends OAuthEndpoint { protected override id: string = 'keycloak'; protected override path: string = '/api/login/keycloak'; protected override redirectPath: string = '/api/login/keycloak-callback'; protected label: string = 'Keycloak'; protected host?: string; protected realm?: string; protected clientID?: string; protected clientSecret?: string; protected userNameClaim?: string; protected keycloakBaseUrl: string; @postConstruct() init() { this.host = this.configuration.getValue('keycloak-host'); this.realm = this.configuration.getValue('keycloak-realm'); this.clientID = this.configuration.getValue('keycloak-client-id'); this.clientSecret = this.configuration.getValue('keycloak-client-secret'); this.userNameClaim = this.configuration.getValue('keycloak-username-claim'); this.label = this.configuration.getValue('keycloak-client-label') ?? 'Keycloak'; this.keycloakBaseUrl = `${this.host}/realms/${this.realm}`; } getProtocolProvider(): AuthProvider { return { endpoint: this.path, name: this.label, type: 'web', label: { code: '', message: this.label, params: [] }, group: ThirdParty }; } shouldActivate(): boolean { return !!this.host && !!this.realm && !!this.clientID; } getStrategy(host: string, port: number): Strategy { return new KeycloakStrategy({ authorizationURL: `${this.keycloakBaseUrl}/protocol/openid-connect/auth`, tokenURL: `${this.keycloakBaseUrl}/protocol/openid-connect/token`, userInfoURL: `${this.keycloakBaseUrl}/protocol/openid-connect/userinfo`, clientID: this.clientID!, clientSecret: this.clientSecret ?? '', scope: ['openid', 'email', 'profile'], callbackURL: this.createRedirectUrl(host, port, this.redirectPath), }, (accessToken: string, refreshToken: string, profile: any, done: VerifyCallback) => { const userInfo = { name: profile[this.userNameClaim ?? 'preferred_username'], email: profile.email, authProvider: this.label, }; done(undefined, userInfo); }); } } type KeycloakStrategyOptions = OAuth2Strategy.StrategyOptions & { userInfoURL: string; } class KeycloakStrategy extends OAuth2Strategy { constructor(protected options: KeycloakStrategyOptions, verify: OAuth2Strategy.VerifyFunction) { super(options, verify); } override async userProfile(accessToken: string, done: (err?: unknown, profile?: any) => void): Promise<void> { fetch(this.options.userInfoURL, { headers: { Authorization: `Bearer ${accessToken}`, }, }).then(async response => { if (!response.ok) { throw new Error(`Failed to fetch user profile: ${response.statusText}`); } done(undefined, await response.json()); }).catch(err => done(err)); } }