Skip to content
This repository has been archived by the owner on May 29, 2024. It is now read-only.

Commit

Permalink
use cluster oauth url to authenticate and login
Browse files Browse the repository at this point in the history
  • Loading branch information
edewit committed Aug 29, 2019
1 parent c4e9357 commit da73a25
Show file tree
Hide file tree
Showing 10 changed files with 22 additions and 223 deletions.
5 changes: 2 additions & 3 deletions frontend/packages/launcher-app/.env.production-api
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@ REACT_APP_CREATOR_API_URL=https://forge.api.openshift.io/creator
REACT_APP_LAUNCHER_API_URL=https://forge.api.openshift.io/api

REACT_APP_AUTHENTICATION=keycloak
REACT_APP_KEYCLOAK_CLIENT_ID=openshiftio-public
REACT_APP_KEYCLOAK_REALM=rh-developers-launch
REACT_APP_KEYCLOAK_URL=https://sso.openshift.io/auth

REACT_APP_OAUTH_OPENSHIFT_CLIENT_ID=openshift-public
12 changes: 3 additions & 9 deletions frontend/packages/launcher-app/src/app/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import axios from 'axios';

import { OpenshiftConfig, KeycloakConfig, GitProviderConfig } from '../auth/types';
import { OpenshiftConfig, GitProviderConfig } from '../auth/types';
import { checkNotNull } from '../client/helpers/preconditions';

function getEnv(env: string | undefined, name: string): string | undefined {
Expand Down Expand Up @@ -32,20 +32,14 @@ function getAuthMode(keycloakUrl?: string, openshiftOAuthUrl?: string) {
return 'no';
}

function getAuthConfig(authMode: string): KeycloakConfig | OpenshiftConfig | undefined {
function getAuthConfig(authMode: string): OpenshiftConfig | undefined {
switch (authMode) {
case 'keycloak':
return {
clientId: requireEnv(process.env.REACT_APP_KEYCLOAK_CLIENT_ID, 'keycloakClientId'),
realm: requireEnv(process.env.REACT_APP_KEYCLOAK_REALM, 'keycloakRealm'),
url: requireEnv(process.env.REACT_APP_KEYCLOAK_URL, 'keycloakUrl'),
gitProvider: (getEnv(process.env.REACT_APP_GIT_PROVIDER, 'gitProvider') || 'github') === 'github' ? 'github' : 'gitea'
} as KeycloakConfig;
case 'oauth-openshift':
const base: OpenshiftConfig = {
openshift: {
clientId: requireEnv(process.env.REACT_APP_OAUTH_OPENSHIFT_CLIENT_ID, 'openshiftOAuthClientId'),
url: requireEnv(process.env.REACT_APP_OAUTH_OPENSHIFT_URL, 'openshiftOAuthUrl'),
url: getEnv(process.env.REACT_APP_OAUTH_OPENSHIFT_URL, 'openshiftOAuthUrl'),
validateTokenUri: `${requireEnv(process.env.REACT_APP_LAUNCHER_API_URL, 'launcherApiUrl')}/services/openshift/user`,
},
loadGitProvider: () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,25 @@ import { KeycloakAuthenticationApi } from './impl/keycloak-authentication-api';
import NoAuthenticationApi from './impl/no-authentication-api';
import { AuthenticationApi } from './authentication-api';
import { OpenshiftAuthenticationApi } from './impl/openshift-authentication-api';
import { KeycloakConfig, OpenshiftConfig } from './types';
import { OpenshiftConfig } from './types';
import { checkNotNull } from '../client/helpers/preconditions';

export { AuthenticationApiContext, useAuthenticationApi, useAuthenticationApiStateProxy } from './auth-context';
export { AuthRouter } from './auth-router';

export function newMockAuthApi() { return new MockAuthenticationApi(); }
export function newKCAuthApi(config: KeycloakConfig) { return new KeycloakAuthenticationApi(config); }
export function newKCAuthApi(config: OpenshiftConfig) { return new KeycloakAuthenticationApi(config); }
export function newOpenshiftAuthApi(config: OpenshiftConfig) { return new OpenshiftAuthenticationApi(config); }
export function newNoAuthApi() { return new NoAuthenticationApi(); }

export function newAuthApi(authenticationMode?: string, config?: OpenshiftConfig|KeycloakConfig): AuthenticationApi {
export function newAuthApi(authenticationMode?: string, config?: OpenshiftConfig): AuthenticationApi {
switch (authenticationMode) {
case 'no':
return new NoAuthenticationApi();
case 'mock':
return new MockAuthenticationApi();
case 'keycloak':
return new KeycloakAuthenticationApi(checkNotNull(config as KeycloakConfig, 'keycloakConfig'));
return new KeycloakAuthenticationApi(checkNotNull(config as OpenshiftConfig, 'keycloakConfig'));
case 'oauth-openshift':
return new OpenshiftAuthenticationApi(checkNotNull(config as OpenshiftConfig, 'openshiftConfig'));
default:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,193 +1,12 @@
import jsSHA from 'jssha';
import Keycloak from 'keycloak-js';
import _ from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { AuthenticationApi } from '../authentication-api';
import { Authorizations, KeycloakConfig, OptionalUser } from '../types';
import { OpenshiftAuthenticationApi } from './openshift-authentication-api';

interface StoredData {
token: string;
refreshToken?: string;
idToken?: string;
}

function takeFirst<R>(fn: (...args: any) => Promise<R>): (...args: any) => Promise<R> {
let pending: Promise<R> | undefined;
let resolve: (val: R) => void;
let reject: (err: Error) => void;
return function(...args) {
if (!pending) {
pending = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
fn(...args).then((val) => {
pending = undefined;
resolve(val);
}, error => {
pending = undefined;
reject(error);
});
}
return pending;
};
}

export class KeycloakAuthenticationApi implements AuthenticationApi {

private _user: OptionalUser;
private onUserChangeListener?: (user: OptionalUser) => void = undefined;

private static base64ToUri(b64: string): string {
return b64.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
}

private readonly keycloak: Keycloak.KeycloakInstance;

constructor(private config: KeycloakConfig, keycloakCoreFactory = Keycloak) {
this.keycloak = keycloakCoreFactory(config);
this.refreshToken = takeFirst(this.refreshToken);
}

public setOnUserChangeListener(listener: (user: OptionalUser) => void) {
this.onUserChangeListener = listener;
}

public init = (): Promise<OptionalUser> => {
return new Promise((resolve, reject) => {
const sessionKC = KeycloakAuthenticationApi.getStoredData();
this.keycloak.init({ ...sessionKC, checkLoginIframe: false })
.error((e) => reject(e))
.success(() => {
this.initUser();
resolve(this._user);
});
this.keycloak.onTokenExpired = () => {
this.refreshToken(true)
.catch(e => console.error(e));
};
});
};

public async getAuthorizations(provider: string): Promise<Authorizations | undefined> {
if (!this._user) {
return;
}
return this._user.authorizationsByProvider['kc'];
}

public get user() {
return this._user;
}

public login = () => {
this.keycloak.login();
return Promise.resolve();
};

public logout = () => {
KeycloakAuthenticationApi.clearStoredData();
this.keycloak.logout();
};
export class KeycloakAuthenticationApi extends OpenshiftAuthenticationApi {

public getAccountManagementLink = () => {
if (!this._user) {
return undefined;
}
return this.keycloak.createAccountUrl();
};

public refreshToken = (force: boolean = false): Promise<OptionalUser> => {
return new Promise<OptionalUser>((resolve, reject) => {
if (this._user) {
console.info('Checking if token needs to be refreshed...');
this.keycloak.updateToken(force ? -1 : 60)
.success(() => {
this.initUser();
resolve(this.user);
})
.error(() => {
this.logout();
reject('Failed to refresh token');
});
} else {
reject('User is not authenticated');
}
});
};

public generateAuthorizationLink = (provider: string = this.config.gitProvider, redirect?: string): string => {
if (!this.user) {
throw new Error('User is not authenticated');
}
if (this.user.accountLink[provider]) {
return this.user.accountLink[provider];
public generateAuthorizationLink(provider?: string, redirect?: string): string {
if (provider !== 'github' && provider !== 'gitea') {
return provider || '';
}
const nonce = uuidv4();
const clientId = this.config.clientId;
const hash = nonce + this.user.sessionState
+ clientId + provider;
const shaObj = new jsSHA('SHA-256', 'TEXT');
shaObj.update(hash);
const hashed = KeycloakAuthenticationApi.base64ToUri(shaObj.getHash('B64'));
// tslint:disable-next-line
const link = `${this.keycloak.authServerUrl}/realms/${this.config.realm}/broker/${provider}/link?nonce=${encodeURI(nonce)}&hash=${hashed}&client_id=${encodeURI(clientId)}&redirect_uri=${encodeURI(redirect || window.location.href)}`;
this.user.accountLink[provider] = link;
return link;
return super.generateAuthorizationLink(provider, redirect);
};

private initUser() {
if (!this.keycloak) {
this._user = {
userName: 'Anonymous',
userPreferredName: 'Anonymous',
authorizationsByProvider: { kc: { Authorization: `Bearer eyJhbGciOiJIUzI1NiJ9.e30.ZRrHA1JJJW8opsbCGfG_HACGpVUMN_a9IV7pAx_Zmeo` } },
sessionState: 'sessionState',
accountLink: {},
};
this.triggerUserChange();
return;
}
if (this.keycloak.token) {
KeycloakAuthenticationApi.setStoredData({
token: this.keycloak.token,
refreshToken: this.keycloak.refreshToken,
idToken: this.keycloak.idToken,
});
this._user = {
userName: _.get(this.keycloak, 'tokenParsed.name'),
userPreferredName: _.get(this.keycloak, 'tokenParsed.preferred_username'),
authorizationsByProvider: { kc: { Authorization: `Bearer ${this.keycloak.token}` } },
sessionState: _.get(this.keycloak, 'tokenParsed.session_state'),
accountLink: {},
};
this.triggerUserChange();
}
}

public get enabled(): boolean {
return true;
}

private triggerUserChange() {
if (this.onUserChangeListener) {
this.onUserChangeListener(this._user);
}
}

private static clearStoredData() {
sessionStorage.clear();
localStorage.removeItem('kc');
}

private static setStoredData(data: StoredData) {
localStorage.setItem('kc', JSON.stringify(data));
}

private static getStoredData(): StoredData | undefined {
const item = localStorage.getItem('kc');
return item && JSON.parse(item);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class OpenshiftAuthenticationApi implements AuthenticationApi {
return this._user.authorizationsByProvider[provider];
}

public generateAuthorizationLink = (provider?: string, redirect?: string): string => {
public generateAuthorizationLink(provider?: string, redirect?: string): string {
const gitProvider = provider || this.gitConfig.gitProvider;
if (gitProvider === 'github') {
const redirectUri = redirect || this.cleanUrl(window.location.href);
Expand Down
2 changes: 1 addition & 1 deletion frontend/packages/launcher-app/src/auth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface OpenshiftConfig {
loadGitProvider: () => Promise<GitProviderConfig>,
openshift: {
clientId: string;
url: string;
url?: string;
validateTokenUri: string;
responseType?: string;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,11 @@ export default class DefaultLauncherClient implements LauncherClient {
}
}
public async ocClusters(): Promise<OpenShiftCluster[]> {
const authorizations = await this.requireOpenShiftAuthorizations();
const requestConfig = await this.getRequestConfig({ authorizations });
const requestConfig = await this.getRequestConfig();
try {
const r = await this.httpService.get<any>(this.config.launcherURL, '/services/openshift/clusters', requestConfig);
const r = await this.httpService.get<any>(this.config.launcherURL, '/services/openshift/clusters/all', requestConfig);
return r.map(c => ({
...c.cluster, connected: c.connected
...c, connected: c.connected
}));
} catch (e) {
if (e.response) {
Expand Down
1 change: 1 addition & 0 deletions frontend/packages/launcher-app/src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export interface OpenShiftCluster {
name: string;
type: string;
consoleUrl?: string;
oauthUrl?: string;
}

export interface GitInfo {
Expand Down
13 changes: 0 additions & 13 deletions frontend/packages/launcher-app/src/hubs/deployment-hub.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { useAuthorizationManager } from '../contexts/authorization-context';
import { OpenshiftClusterLoader, OpenshiftClustersLoader } from '../loaders/openshiftcluster-loader';
import { ClusterPicker, ClusterPickerValue } from '../pickers/cluster-picker';
import { FormHub, SpecialValue, FormPanel, DescriptiveHeader, OverviewEmpty, OverviewComplete, ExternalLink } from '@launcher/component';
import { useAuthenticationApi } from '../auth/auth-context';

export interface DeploymentFormValue {
clusterPickerValue?: ClusterPickerValue;
Expand All @@ -15,18 +14,6 @@ export const DeploymentHub: FormHub<DeploymentFormValue> = {
title: 'OpenShift Deployment',
checkCompletion: value => !!value.clusterPickerValue && ClusterPicker.checkCompletion(value.clusterPickerValue),
Overview: props => {
const authApi = useAuthenticationApi();
if (!authApi.user && authApi.enabled) {
return (
<OverviewEmpty
id={DeploymentHub.id}
title="You need to login for OpenShift deployment"
action={<Button variant="primary" onClick={authApi.login}>Login</Button>}
>
When you are logged in we can deploy this application on your cluster.
</OverviewEmpty>
);
}
if (!DeploymentHub.checkCompletion(props.value)) {
return (
<OverviewEmpty
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export const ClusterPicker: Picker<ClusterPickerProps, ClusterPickerValue> = {
<Button
// @ts-ignore
component="a"
href={props.authorizationLinkGenerator(cluster.id)}
href={props.authorizationLinkGenerator(cluster.oauthUrl)}
target="_blank"
>
Authorize
Expand Down

0 comments on commit da73a25

Please sign in to comment.