diff --git a/frontend/.env.docker.staging-api b/frontend/.env.docker.staging-api index 5149f467b..3bdac8c8e 100644 --- a/frontend/.env.docker.staging-api +++ b/frontend/.env.docker.staging-api @@ -1,6 +1,5 @@ LAUNCHER_BACKEND_URL=https://forge.api.prod-preview.openshift.io/api LAUNCHER_CREATOR_URL=https://launch.prod-preview.openshift.io/launch/creator -LAUNCHER_KEYCLOAK_URL=https://sso.openshift.io/auth -LAUNCHER_KEYCLOAK_CLIENT_ID=openshiftio-public -LAUNCHER_KEYCLOAK_REALM=rh-developers-launch +REACT_APP_AUTHENTICATION=oauth-cluster +REACT_APP_OAUTH_OPENSHIFT_CLIENT_ID=openshift-public diff --git a/frontend/packages/launcher-app/.env.production b/frontend/packages/launcher-app/.env.production index 2c0c8c24f..0bed0ff1a 100644 --- a/frontend/packages/launcher-app/.env.production +++ b/frontend/packages/launcher-app/.env.production @@ -1,4 +1,5 @@ REACT_APP_CREATOR_API_URL=/launch/creator REACT_APP_LAUNCHER_API_URL=/launch/api -REACT_APP_KEYCLOAK_CLIENT_ID=openshiftio-public +REACT_APP_AUTHENTICATION=oauth-cluster +REACT_APP_OAUTH_OPENSHIFT_CLIENT_ID=openshift-public diff --git a/frontend/packages/launcher-app/.env.production-api b/frontend/packages/launcher-app/.env.production-api index 91373f4d2..bd8683e83 100644 --- a/frontend/packages/launcher-app/.env.production-api +++ b/frontend/packages/launcher-app/.env.production-api @@ -1,7 +1,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_AUTHENTICATION=oauth-cluster +REACT_APP_OAUTH_OPENSHIFT_CLIENT_ID=openshift-public diff --git a/frontend/packages/launcher-app/.env.staging-api b/frontend/packages/launcher-app/.env.staging-api index 974ba172f..f6cc232c6 100644 --- a/frontend/packages/launcher-app/.env.staging-api +++ b/frontend/packages/launcher-app/.env.staging-api @@ -1,7 +1,5 @@ REACT_APP_CREATOR_API_URL=https://forge.api.prod-preview.openshift.io/creator REACT_APP_LAUNCHER_API_URL=https://forge.api.prod-preview.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_AUTHENTICATION=oauth-cluster +REACT_APP_OAUTH_OPENSHIFT_CLIENT_ID=openshift-public diff --git a/frontend/packages/launcher-app/package.json b/frontend/packages/launcher-app/package.json index 83c728b9b..b6bca071a 100644 --- a/frontend/packages/launcher-app/package.json +++ b/frontend/packages/launcher-app/package.json @@ -30,7 +30,6 @@ "react-use-sessionstorage": "1.0.2", "axios": "0.19.0", "jssha": "2.3.1", - "keycloak-js": "6.0.1", "query-string": "6.8.1", "uuid": "3.3.2", "lscache": "1.3.0", diff --git a/frontend/packages/launcher-app/src/app/config.ts b/frontend/packages/launcher-app/src/app/config.ts index 18fe9bf61..8db34119f 100644 --- a/frontend/packages/launcher-app/src/app/config.ts +++ b/frontend/packages/launcher-app/src/app/config.ts @@ -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 { @@ -18,34 +18,25 @@ function requireEnv(env: string | undefined, name: string): string { return checkNotNull(getEnv(env, name), `process.env.${name}`); } -function getAuthMode(keycloakUrl?: string, openshiftOAuthUrl?: string) { +function getAuthMode(openshiftOAuthUrl?: string) { const authMode = getEnv(process.env.REACT_APP_AUTHENTICATION, 'authMode'); if (authMode) { return authMode; } - if (keycloakUrl) { - return 'keycloak'; - } if (openshiftOAuthUrl) { return 'oauth-openshift' } 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-cluster': 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: () => { @@ -89,9 +80,8 @@ function getAuthConfig(authMode: string): KeycloakConfig | OpenshiftConfig | und export const publicUrl = process.env.PUBLIC_URL && `${process.env.PUBLIC_URL}/`; -export const keycloakUrl = getEnv(process.env.REACT_APP_KEYCLOAK_URL, 'keycloakUrl'); export const openshiftOAuthUrl = getEnv(process.env.REACT_APP_OAUTH_OPENSHIFT_URL, 'openshiftOAuthUrl'); -export const authMode = getAuthMode(keycloakUrl, openshiftOAuthUrl) +export const authMode = getAuthMode(openshiftOAuthUrl) export const authConfig = getAuthConfig(authMode); diff --git a/frontend/packages/launcher-app/src/app/launcher-app.tsx b/frontend/packages/launcher-app/src/app/launcher-app.tsx index 17b279c32..2e447f3fb 100755 --- a/frontend/packages/launcher-app/src/app/launcher-app.tsx +++ b/frontend/packages/launcher-app/src/app/launcher-app.tsx @@ -3,18 +3,18 @@ import React from 'react'; import { Redirect, Route, Switch } from 'react-router'; import { BrowserRouter } from 'react-router-dom'; import { AuthenticationApiContext, useAuthenticationApiStateProxy } from '../auth/auth-context'; -import { AuthRouter, newAuthApi } from '../auth/authentication-api-factory'; +import { newAuthApi } from '../auth/authentication-api-factory'; import { createRouterLink, getRequestedRoute, useRouter } from '../router/use-router'; import { authConfig, authMode, creatorApiUrl, launcherApiUrl, publicUrl } from './config'; import './launcher-app.scss'; import { Layout } from './layout'; -import { LoginPage } from './login-page'; import { LauncherMenu } from '../launcher/launcher'; import { CreateNewAppFlow } from '../flows/create-new-app-flow'; import { ImportExistingFlow } from '../flows/import-existing-flow'; import { DeployExampleAppFlow } from '../flows/deploy-example-app-flow'; import { DataLoader } from '@launcher/component'; import { LauncherDepsProvider } from '../contexts/launcher-client-provider'; +import { LoginPage } from './login-page'; function Routes(props: {}) { @@ -42,11 +42,12 @@ function Routes(props: {}) { const DeployExampleAppFlowRoute = () => ({onCancel => }); return ( + - + ); } @@ -78,9 +79,7 @@ export function LauncherApp() { creatorUrl={creatorApiUrl} launcherUrl={launcherApiUrl} > - - - + diff --git a/frontend/packages/launcher-app/src/app/login-page.module.scss b/frontend/packages/launcher-app/src/app/login-page.module.scss index 500ef2f93..fdbd21ef4 100644 --- a/frontend/packages/launcher-app/src/app/login-page.module.scss +++ b/frontend/packages/launcher-app/src/app/login-page.module.scss @@ -1,5 +1,10 @@ $assetPath: "./assets/logo"; +.main { + background-color: #fff; + margin:-20px +} + .intro { background-image: url(#{$assetPath}/background.png); background-size: cover; @@ -76,7 +81,7 @@ button.loginButton { button.loginButton { font-size: 10pt; } - + .container { margin-top: 0; display: initial; diff --git a/frontend/packages/launcher-app/src/app/login-page.tsx b/frontend/packages/launcher-app/src/app/login-page.tsx index d4b54ddab..8b86e89da 100644 --- a/frontend/packages/launcher-app/src/app/login-page.tsx +++ b/frontend/packages/launcher-app/src/app/login-page.tsx @@ -1,21 +1,22 @@ import { Button, Card, CardBody, CardFooter, CardHeader, PageSection, PageSectionVariants, Text, TextContent, TextVariants } from '@patternfly/react-core'; import { ExternalLinkSquareAltIcon } from '@patternfly/react-icons'; import * as React from 'react'; -import { useAuthenticationApi } from '../auth/auth-context'; -import { Layout } from './layout'; import style from './login-page.module.scss'; import { NewAppRuntimesLoader } from '../loaders/new-app-runtimes-loaders'; import { PropertyValue } from '../client/types'; +import { useRouter, createRouterLink } from '../router/use-router'; function LoginCard() { - const auth = useAuthenticationApi(); + const router = useRouter(); + const homeLink = createRouterLink(router, '/home'); + return (

When you click on start, you will first have to login or register an account for free with the Red Hat Developer Program.

-
@@ -41,7 +42,7 @@ function Runtime(props: RuntimeProps) { } export const LoginPage = () => ( - +

Launcher

@@ -70,5 +71,5 @@ export const LoginPage = () => ( {runtimes => runtimes.map(r => ())} - +
); diff --git a/frontend/packages/launcher-app/src/auth/authentication-api-factory.ts b/frontend/packages/launcher-app/src/auth/authentication-api-factory.ts index 471081e52..b7672135e 100644 --- a/frontend/packages/launcher-app/src/auth/authentication-api-factory.ts +++ b/frontend/packages/launcher-app/src/auth/authentication-api-factory.ts @@ -1,27 +1,27 @@ import MockAuthenticationApi from './impl/mock-authentication-api'; -import { KeycloakAuthenticationApi } from './impl/keycloak-authentication-api'; +import { ClusterAuthenticationApi } from './impl/cluster-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 ClusterAuthenticationApi(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')); + case 'oauth-cluster': + return new ClusterAuthenticationApi(checkNotNull(config as OpenshiftConfig, 'oauthConfig')); case 'oauth-openshift': return new OpenshiftAuthenticationApi(checkNotNull(config as OpenshiftConfig, 'openshiftConfig')); default: diff --git a/frontend/packages/launcher-app/src/auth/impl/cluster-authentication-api.ts b/frontend/packages/launcher-app/src/auth/impl/cluster-authentication-api.ts new file mode 100644 index 000000000..6592ad724 --- /dev/null +++ b/frontend/packages/launcher-app/src/auth/impl/cluster-authentication-api.ts @@ -0,0 +1,12 @@ +import { OpenshiftAuthenticationApi } from './openshift-authentication-api'; + +export class ClusterAuthenticationApi extends OpenshiftAuthenticationApi { + + public generateAuthorizationLink(provider?: string, redirect?: string): string { + if (provider !== 'github' && provider !== 'gitea') { + return provider || ''; + } + return super.generateAuthorizationLink(provider, redirect); + }; + +} diff --git a/frontend/packages/launcher-app/src/auth/impl/keycloak-authentication-api.ts b/frontend/packages/launcher-app/src/auth/impl/keycloak-authentication-api.ts deleted file mode 100644 index a7ae06f93..000000000 --- a/frontend/packages/launcher-app/src/auth/impl/keycloak-authentication-api.ts +++ /dev/null @@ -1,193 +0,0 @@ -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'; - -interface StoredData { - token: string; - refreshToken?: string; - idToken?: string; -} - -function takeFirst(fn: (...args: any) => Promise): (...args: any) => Promise { - let pending: Promise | 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 => { - 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 { - 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(); - }; - - public getAccountManagementLink = () => { - if (!this._user) { - return undefined; - } - return this.keycloak.createAccountUrl(); - }; - - public refreshToken = (force: boolean = false): Promise => { - return new Promise((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]; - } - 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; - }; - - 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); - } -} diff --git a/frontend/packages/launcher-app/src/auth/impl/mock-authentication-api.ts b/frontend/packages/launcher-app/src/auth/impl/mock-authentication-api.ts index 5d17772c2..2765d30cb 100644 --- a/frontend/packages/launcher-app/src/auth/impl/mock-authentication-api.ts +++ b/frontend/packages/launcher-app/src/auth/impl/mock-authentication-api.ts @@ -33,7 +33,7 @@ export default class MockAuthenticationApi implements AuthenticationApi { } public generateAuthorizationLink = (provider?: string, redirect?: string): string => { - return `https://authorize/${provider}`; + return `${provider}`; }; public login = (): void => { diff --git a/frontend/packages/launcher-app/src/auth/impl/openshift-authentication-api.ts b/frontend/packages/launcher-app/src/auth/impl/openshift-authentication-api.ts index 3c540cb76..b42359869 100644 --- a/frontend/packages/launcher-app/src/auth/impl/openshift-authentication-api.ts +++ b/frontend/packages/launcher-app/src/auth/impl/openshift-authentication-api.ts @@ -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); diff --git a/frontend/packages/launcher-app/src/auth/types.ts b/frontend/packages/launcher-app/src/auth/types.ts index 9539784be..c469c0792 100644 --- a/frontend/packages/launcher-app/src/auth/types.ts +++ b/frontend/packages/launcher-app/src/auth/types.ts @@ -1,15 +1,9 @@ -export interface KeycloakConfig { - clientId: string; - realm: string; - url: string; - gitProvider: 'gitea' | 'github'; -} export interface OpenshiftConfig { loadGitProvider: () => Promise, openshift: { clientId: string; - url: string; + url?: string; validateTokenUri: string; responseType?: string; }; diff --git a/frontend/packages/launcher-app/src/client/impl/default.launcher.client.ts b/frontend/packages/launcher-app/src/client/impl/default.launcher.client.ts index 368a0a85f..ba5ca6872 100644 --- a/frontend/packages/launcher-app/src/client/impl/default.launcher.client.ts +++ b/frontend/packages/launcher-app/src/client/impl/default.launcher.client.ts @@ -146,12 +146,11 @@ export default class DefaultLauncherClient implements LauncherClient { } } public async ocClusters(): Promise { - const authorizations = await this.requireOpenShiftAuthorizations(); - const requestConfig = await this.getRequestConfig({ authorizations }); + const requestConfig = await this.getRequestConfig(); try { - const r = await this.httpService.get(this.config.launcherURL, '/services/openshift/clusters', requestConfig); + const r = await this.httpService.get(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) { diff --git a/frontend/packages/launcher-app/src/client/types.ts b/frontend/packages/launcher-app/src/client/types.ts index 4ab1fa24f..aa27d3b92 100644 --- a/frontend/packages/launcher-app/src/client/types.ts +++ b/frontend/packages/launcher-app/src/client/types.ts @@ -114,6 +114,7 @@ export interface OpenShiftCluster { name: string; type: string; consoleUrl?: string; + oauthUrl?: string; } export interface GitInfo { diff --git a/frontend/packages/launcher-app/src/flows/launch-flow.tsx b/frontend/packages/launcher-app/src/flows/launch-flow.tsx index fbdeb9245..a913d340c 100644 --- a/frontend/packages/launcher-app/src/flows/launch-flow.tsx +++ b/frontend/packages/launcher-app/src/flows/launch-flow.tsx @@ -112,7 +112,7 @@ export function LaunchFlow(props: LaunchFlowProps) { const [run, setRun] = useState({status: Status.EDITION, statusMessages: []}); const client = useLauncherClient(); const analytics = useAnalytics(); - + useEffect(() => analytics.event('Flow', 'Open', props.id), [analytics, props.id]); const canDownload = props.canDownload === undefined || props.canDownload; diff --git a/frontend/packages/launcher-app/src/pickers/cluster-picker.tsx b/frontend/packages/launcher-app/src/pickers/cluster-picker.tsx index 1072a6208..812fa3e08 100644 --- a/frontend/packages/launcher-app/src/pickers/cluster-picker.tsx +++ b/frontend/packages/launcher-app/src/pickers/cluster-picker.tsx @@ -110,7 +110,7 @@ export const ClusterPicker: Picker = {