open-collaboration-server
Version:
Open Collaboration Server implementation, part of the Open Collaboration Tools project
108 lines (87 loc) • 3.97 kB
text/typescript
// ******************************************************************************
// 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';
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;
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));
}
}