Skip to content

Commit

Permalink
#1591 added oidc provider configuration field "extractBearerTokenFrom…
Browse files Browse the repository at this point in the history
…" to define from where to extract the Bearer token

* also used "state" to propagate back to client after redirect if main or oauth SSO was done, or both
  • Loading branch information
thjaeckle committed Sep 27, 2024
1 parent 8386ff9 commit 4d3f27a
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 29 deletions.
4 changes: 2 additions & 2 deletions ui/modules/environments/authorization.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ <h5>Main authentication</h5>
<div class="input-group">
<label for="oidcProvider" class="input-group-text"><small>SSO provider</small></label>
<select class="form-select form-select-sm me-2" id="oidcProvider"></select>
<button class="btn btn-outline-secondary btn-sm" id="main-oidc-login">Login</button>
<button class="btn btn-outline-secondary btn-sm" id="main-oidc-login" data-bs-dismiss="modal">Login</button>
<button class="btn btn-outline-secondary btn-sm" id="main-oidc-logout" data-bs-dismiss="modal">Logout</button>
</div>
</div>
Expand Down Expand Up @@ -92,7 +92,7 @@ <h5>DevOps authentication</h5>
<div class="input-group">
<label for="devOpsOidcProvider" class="input-group-text"><small>SSO provider</small></label>
<select class="form-select form-select-sm me-2" id="devOpsOidcProvider"></select>
<button class="btn btn-outline-secondary btn-sm" id="devops-oidc-login">Login</button>
<button class="btn btn-outline-secondary btn-sm" id="devops-oidc-login" data-bs-dismiss="modal">Login</button>
<button class="btn btn-outline-secondary btn-sm" id="devops-oidc-logout" data-bs-dismiss="modal">Logout</button>
</div>
</div>
Expand Down
84 changes: 66 additions & 18 deletions ui/modules/environments/authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ import authorizationHTML from './authorization.html';
/* eslint-disable prefer-const */
/* eslint-disable require-jsdoc */
import * as Environments from './environments.js';
import { AuthMethod, OidcAuthSettings, URL_OIDC_PROVIDER, URL_PRIMARY_ENVIRONMENT_NAME } from './environments.js';
import {
AuthMethod,
OidcAuthSettings,
OidcProviderConfiguration,
URL_OIDC_PROVIDER,
URL_PRIMARY_ENVIRONMENT_NAME
} from './environments.js';

let dom = {
mainBearerSection: null,
Expand All @@ -41,6 +47,15 @@ let dom = {
collapseConnections: null,
};

type OidcState = {
mainAuth: boolean,
devopsAuth: boolean
}

function OidcState(state: OidcState): void {
Object.assign(this, state);
}

let _forDevops = false;

document.getElementById('authorizationHTML').innerHTML = authorizationHTML;
Expand Down Expand Up @@ -108,8 +123,10 @@ export function ready() {
e.preventDefault();
let environment = Environments.current();
environment.authSettings.main.oidc.provider = dom.oidcProvider.value;
let alreadyLoggedIn = await performSingleSignOn(environment.authSettings.main.oidc)
Environments.saveEnvironmentsToLocalStorage();
let alreadyLoggedIn = await performSingleSignOn(true)
if (alreadyLoggedIn) {
environment.authSettings.main.method = AuthMethod.oidc
showInfoToast('You are already logged in')
}
await Environments.environmentsJsonChanged(false);
Expand All @@ -119,15 +136,16 @@ export function ready() {
let environment = Environments.current();
environment.authSettings.main.oidc.provider = dom.oidcProvider.value;
await performSingleSignOut(environment.authSettings.main.oidc)
await Environments.environmentsJsonChanged(false);
};

document.getElementById('devops-oidc-login').onclick = async (e) => {
e.preventDefault();
let environment = Environments.current();
environment.authSettings.devops.oidc.provider = dom.devOpsOidcProvider.value;
let alreadyLoggedIn = await performSingleSignOn(environment.authSettings.devops.oidc)
Environments.saveEnvironmentsToLocalStorage();
let alreadyLoggedIn = await performSingleSignOn(false)
if (alreadyLoggedIn) {
environment.authSettings.devops.method = AuthMethod.oidc
showInfoToast('You are already logged in')
}
await Environments.environmentsJsonChanged(false);
Expand All @@ -137,7 +155,6 @@ export function ready() {
let environment = Environments.current();
environment.authSettings.devops.oidc.provider = dom.devOpsOidcProvider.value;
await performSingleSignOut(environment.authSettings.devops.oidc)
await Environments.environmentsJsonChanged(false);
};
}

Expand All @@ -150,15 +167,27 @@ function isSsoCallbackRequest(urlSearchParams?: URLSearchParams): boolean {
return requestContainedCode && requestContainedState;
}

async function handleSingleSignOnCallback(oidc: OidcAuthSettings) {
async function handleSingleSignOnCallback(urlSearchParams: URLSearchParams) {
let environment = Environments.current();
const settings: UserManagerSettings = environment.authSettings.oidc.providers[oidc.provider];
let sameProviderForMainAndDevops =
environment.authSettings?.main?.oidc.provider == environment.authSettings?.devops?.oidc.provider;
const oidcProviderId = urlSearchParams.get(URL_OIDC_PROVIDER) || environment.authSettings?.main?.oidc.provider;
let oidcProvider: OidcProviderConfiguration = environment.authSettings.oidc.providers[oidcProviderId];
const settings: UserManagerSettings = oidcProvider;
if (settings !== undefined && settings !== null) {
const userManager = new UserManager(settings);
try {
let user = await userManager.signinCallback(window.location.href)
if (user) {
oidc.bearerToken = user.access_token
let oidcState = user.state as OidcState
if (oidcState.mainAuth) {
environment.authSettings.main.method = AuthMethod.oidc
environment.authSettings.main.oidc.bearerToken = user[oidcProvider.extractBearerTokenFrom]
}
if (oidcState.devopsAuth) {
environment.authSettings.devops.method = AuthMethod.oidc
environment.authSettings.devops.oidc.bearerToken = user[oidcProvider.extractBearerTokenFrom]
}
window.history.replaceState(null, null, `${settings.redirect_uri}?${user.url_state}`)
await Environments.environmentsJsonChanged(false)
}
Expand All @@ -168,26 +197,44 @@ async function handleSingleSignOnCallback(oidc: OidcAuthSettings) {
}
}

async function performSingleSignOn(oidc: OidcAuthSettings): Promise<boolean> {
async function performSingleSignOn(forMainAuth: boolean): Promise<boolean> {
let environment = Environments.current();
const settings: UserManagerSettings = environment.authSettings.oidc.providers[oidc.provider];
let oidc: OidcAuthSettings;
if (forMainAuth) {
oidc = environment.authSettings?.main?.oidc;
} else {
oidc = environment.authSettings?.devops?.oidc;
}
let sameProviderForMainAndDevops =
environment.authSettings?.main?.oidc.provider == environment.authSettings?.devops?.oidc.provider;
let oidcProvider = environment.authSettings.oidc.providers[oidc.provider];
const settings: UserManagerSettings = oidcProvider;
if (settings !== undefined && settings !== null) {
const urlSearchParams: URLSearchParams = new URLSearchParams(window.location.search);
const userManager = new UserManager(settings);
if (isSsoCallbackRequest(urlSearchParams)) {
await handleSingleSignOnCallback(oidc)
await handleSingleSignOnCallback(urlSearchParams)
return false
} else {
let user = await userManager.getUser();
if (user?.access_token !== undefined || user?.expired === true) {
if (user?.[oidcProvider.extractBearerTokenFrom] !== undefined || user?.expired === true) {
// a user is still logged in via a valid token stored in the browser's session storage
oidc.bearerToken = user?.access_token
if (sameProviderForMainAndDevops) {
environment.authSettings.main.oidc.bearerToken = user[oidcProvider.extractBearerTokenFrom]
environment.authSettings.devops.oidc.bearerToken = user[oidcProvider.extractBearerTokenFrom]
} else {
oidc.bearerToken = user[oidcProvider.extractBearerTokenFrom]
}
return true
} else {
urlSearchParams.set(URL_PRIMARY_ENVIRONMENT_NAME, Environments.currentEnvironmentSelector())
urlSearchParams.set(URL_OIDC_PROVIDER, oidc.provider)
try {
await userManager.signinRedirect({
state: new OidcState({
mainAuth: forMainAuth || sameProviderForMainAndDevops,
devopsAuth: !forMainAuth || sameProviderForMainAndDevops
}),
url_state: urlSearchParams.toString()
});
} catch (e) {
Expand Down Expand Up @@ -222,7 +269,7 @@ async function performSingleSignOut(oidc: OidcAuthSettings) {
showError(e)
} finally {
oidc.bearerToken = undefined
await Environments.environmentsJsonChanged(false)
Environments.saveEnvironmentsToLocalStorage();
}
}
}
Expand All @@ -231,7 +278,7 @@ function dynamicallyShowOrHideSection(sectionEnabled: boolean, section: HTMLElem
if (!sectionEnabled && section) {
section.style.display = 'none'
} else if (sectionEnabled) {
section.style.display = 'inherit'
section.style.display = null
}
}

Expand Down Expand Up @@ -281,14 +328,15 @@ export async function onEnvironmentChanged(initialPageLoad: boolean) {
environment.authSettings.devops.bearer.enabled;
dynamicallyShowOrHideSection(anyDevOpsAuthEnabled, dom.devOpsBasicSection.parentElement);

let urlSearchParams = new URLSearchParams(window.location.search);
if (initialPageLoad &&
environment.authSettings?.main?.method === AuthMethod.oidc &&
environment.authSettings?.main?.oidc?.autoSso === true
) {
await performSingleSignOn(environment.authSettings?.main?.oidc);
await performSingleSignOn(true);
await Environments.environmentsJsonChanged(false);
} else if (isSsoCallbackRequest()) {
await handleSingleSignOnCallback(environment.authSettings?.main?.oidc);
} else if (isSsoCallbackRequest(urlSearchParams)) {
await handleSingleSignOnCallback(urlSearchParams);
}

API.setAuthHeader(_forDevops);
Expand Down
3 changes: 2 additions & 1 deletion ui/modules/environments/environmentTemplates.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
"method": "basic",
"oidc": {
"enabled": false,
"defaultProvider": "fake"
"defaultProvider": null
},
"basic": {
"enabled": false,
Expand Down Expand Up @@ -169,6 +169,7 @@
"providers": {
"fake": {
"displayName": "Fake IDP to test",
"extractBearerTokenFrom": "access_token",
"authority": "http://localhost:9900/fake",
"client_id": "some-client-id",
"redirect_uri": "http://localhost:8000",
Expand Down
20 changes: 14 additions & 6 deletions ui/modules/environments/environments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,11 @@ type MainAuthSettings = CommonAuthSettings & {
pre: PreAuthSettings
}

type OidcProviderConfiguration = UserManagerSettings /* from 'oidc-client-ts' */ & {
export type OidcProviderConfiguration = UserManagerSettings /* from 'oidc-client-ts' */ & {
/** The name used in the drop-down list of available OIDC providers */
displayName: string
displayName: string,
/** Configures the field to use as 'Bearer' token from the response of the OIDC provider's /token endpoint, e.g. either "access_token" or "id_token" */
extractBearerTokenFrom: string
}

type AuthSettings = {
Expand Down Expand Up @@ -134,7 +136,6 @@ type Environment = {
recentPolicyIds?: string[],
}

let urlSearchParams: URLSearchParams;
let environments: Record<string, Environment>;
let selectedEnvName: string;

Expand Down Expand Up @@ -191,7 +192,6 @@ export async function ready() {

Utils.addValidatorToTable(dom.tbodyEnvironments, dom.tableValidationEnvironments);

urlSearchParams = new URLSearchParams(window.location.search);
environments = await loadEnvironmentTemplates();

settingsEditor = Utils.createAceEditor('settingsEditor', 'ace/mode/json', true);
Expand Down Expand Up @@ -219,8 +219,9 @@ export async function ready() {
}

async function onEnvironmentSelectorChange() {
let urlSearchParams = new URLSearchParams(window.location.search);
urlSearchParams.set(URL_PRIMARY_ENVIRONMENT_NAME, currentEnvironmentSelector());
window.history.replaceState({}, '', `${window.location.pathname}?${urlSearchParams}`);
window.history.replaceState(null, null, `${window.location.pathname}?${urlSearchParams}`);
await notifyAll(false);
}

Expand Down Expand Up @@ -291,17 +292,23 @@ async function onUpdateEnvironmentClick(event) {
}
}

export async function environmentsJsonChanged(initialPageLoad: boolean, modifiedField = null) {
export function saveEnvironmentsToLocalStorage() {
environments && localStorage.setItem(STORAGE_KEY, JSON.stringify(environments));
}

export async function environmentsJsonChanged(initialPageLoad: boolean, modifiedField = null) {
updateEnvSelector();

saveEnvironmentsToLocalStorage();

updateEnvEditors();
updateEnvTable();

await notifyAll(initialPageLoad, modifiedField);

function updateEnvSelector() {
let activeEnvironment = dom.environmentSelector.value;
let urlSearchParams = new URLSearchParams(window.location.search);
if (!activeEnvironment) {
activeEnvironment = urlSearchParams.get(URL_PRIMARY_ENVIRONMENT_NAME);
}
Expand Down Expand Up @@ -365,6 +372,7 @@ async function loadEnvironmentTemplates() {
let fromURL;
let fromLocalStorage;

let urlSearchParams = new URLSearchParams(window.location.search);
let environmentsURL = urlSearchParams.get(URL_ENVIRONMENTS);
if (environmentsURL) {
try {
Expand Down
7 changes: 5 additions & 2 deletions ui/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ type MainAuthSettings = CommonAuthSettings & {

type OidcProviderConfiguration = UserManagerSettings /* from 'oidc-client-ts' */ & {
/** The name used in the drop-down list of available OIDC providers */
displayName: string
displayName: string,
/** Configures the field to use as 'Bearer' token from the response of the OIDC provider's /token endpoint, e.g. either "access_token" or "id_token" */
extractBearerTokenFrom: string
}

export enum AuthMethod {
Expand Down Expand Up @@ -300,7 +302,8 @@ An example environment JSON file could look like:
"oidc": {
"providers": {
"fake": {
"displayName": "Fake IDP to test",
"displayName": "Fake IDP to test",
"extractBearerTokenFrom": "access_token",
"authority": "http://localhost:9900/fake",
"client_id": "some-client-id",
"redirect_uri": "http://localhost:8000",
Expand Down

0 comments on commit 4d3f27a

Please sign in to comment.