diff --git a/apps/maarch_entreprise/xml/keycloakConfig.xml b/apps/maarch_entreprise/xml/keycloakConfig.xml index 5c67686d90cd54d9982eb78edd993fd9032a019e..0e6d13f5aea0fde0fbe083a27cb2054f11846785 100644 --- a/apps/maarch_entreprise/xml/keycloakConfig.xml +++ b/apps/maarch_entreprise/xml/keycloakConfig.xml @@ -4,8 +4,9 @@ <REALM></REALM> <CLIENT_ID></CLIENT_ID> <CLIENT_SECRET></CLIENT_SECRET> - <REDIRECT_URI>http://demo.maarchcourrier.com/apps/maarch_entreprise/index.php?display=true&page=login</REDIRECT_URI> <!-- Should be the url of Maarch Courrier's login page --> + <REDIRECT_URI>http://demo.maarchcourrier.com/dist/index.html#/login</REDIRECT_URI> <!-- Should be the url of Maarch Courrier's login page --> <ENCRYPTION_ALGORITHM></ENCRYPTION_ALGORITHM> <ENCRYPTION_KEY_PATH></ENCRYPTION_KEY_PATH> <ENCRYPTION_KEY></ENCRYPTION_KEY> + <SCOPE></SCOPE> </ROOT> diff --git a/apps/maarch_entreprise/xml/login_method.xml b/apps/maarch_entreprise/xml/login_method.xml index b185b6d5e13e26ca1599cff06e8c8582c4f05d28..8124b63e3ab329f0a9ecb506679bd8c9aa00d3bc 100755 --- a/apps/maarch_entreprise/xml/login_method.xml +++ b/apps/maarch_entreprise/xml/login_method.xml @@ -20,8 +20,6 @@ </METHOD> <METHOD> <ID>keycloak</ID> - <NAME>Keycloak</NAME> - <SCRIPT>keycloakConnect.php</SCRIPT> <ENABLED>false</ENABLED> </METHOD> <METHOD> diff --git a/src/core/controllers/AuthenticationController.php b/src/core/controllers/AuthenticationController.php index 397c807dce2446f12dcbe2bf49203bcabbfcdbc7..373740298fe4e18e8bb9696927d726efd9694881 100755 --- a/src/core/controllers/AuthenticationController.php +++ b/src/core/controllers/AuthenticationController.php @@ -26,6 +26,7 @@ use SrcCore\models\AuthenticationModel; use SrcCore\models\CoreConfigModel; use SrcCore\models\PasswordModel; use SrcCore\models\ValidatorModel; +use Stevenmaguire\OAuth2\Client\Provider\Keycloak; use User\models\UserModel; class AuthenticationController @@ -58,16 +59,27 @@ class AuthenticationController $port = (string)$casConfiguration->WEB_CAS_PORT; $uri = (string)$casConfiguration->WEB_CAS_CONTEXT; $authUri = "https://{$hostname}:{$port}{$uri}/login?service=" . UrlController::getCoreUrl() . 'dist/index.html#/login'; + } elseif ($loggingMethod['id'] == 'keycloak') { + $keycloakConfig = CoreConfigModel::getKeycloakConfiguration(); + $provider = new Keycloak($keycloakConfig); + $authUri = $provider->getAuthorizationUrl(['scope' => $keycloakConfig['scope']]); + $keycloakState = $provider->getState(); } - return $response->withJson([ + $return = [ 'instanceId' => $hashedPath, 'applicationName' => $appName, 'loginMessage' => $parameter['param_value_string'] ?? null, 'changeKey' => $encryptKey == 'Security Key Maarch Courrier #2008', 'authMode' => $loggingMethod['id'], 'authUri' => $authUri - ]); + ]; + + if (!empty($keycloakState)) { + $return['keycloakState'] = $keycloakState; + } + + return $response->withJson($return); } public function getValidUrl(Request $request, Response $response) @@ -263,6 +275,16 @@ class AuthenticationController if (!AuthenticationController::isUserAuthorized(['login' => $login])) { return $response->withStatus(403)->withJson(['errors' => 'Authentication Failed']); } + } elseif ($loggingMethod['id'] == 'keycloak') { + $queryParams = $request->getQueryParams(); + $authenticated = AuthenticationController::keycloakConnection(['code' => $queryParams['code']]); + if (!empty($authenticated['errors'])) { + return $response->withStatus(401)->withJson(['errors' => $authenticated['errors']]); + } + $login = strtolower($authenticated['login']); + if (!AuthenticationController::isUserAuthorized(['login' => $login])) { + return $response->withStatus(403)->withJson(['errors' => 'Authentication unauthorized']); + } } else { return $response->withStatus(403)->withJson(['errors' => 'Logging method unauthorized']); } @@ -312,10 +334,14 @@ class AuthenticationController { $loggingMethod = CoreConfigModel::getLoggingMethod(); + $res = ['logoutUrl' => null]; if ($loggingMethod['id'] == 'cas') { $res = AuthenticationController::casDisconnection(); + } elseif ($loggingMethod['id'] == 'keycloak') { + $res = AuthenticationController::keycloakDisconnection(); } - return $response->withJson(['logoutUrl' => $res['logoutUrl'], 'redirectUrl' => $res['redirectUrl']]); + + return $response->withJson(['logoutUrl' => $res['logoutUrl']]); } private static function standardConnection(array $args) @@ -461,7 +487,69 @@ class AuthenticationController \phpCAS::setFixedServiceURL(UrlController::getCoreUrl() . 'dist/index.html'); \phpCAS::setNoClearTicketsFromUrl(); $logoutUrl = \phpCAS::getServerLogoutURL(); - return ['logoutUrl' => $logoutUrl, 'redirectUrl' => UrlController::getCoreUrl() . 'dist/index.html']; + return ['logoutUrl' => $logoutUrl]; + } + + private static function keycloakConnection(array $args) + { + $keycloakConfig = CoreConfigModel::getKeycloakConfiguration(); + + if (empty($keycloakConfig) || empty($keycloakConfig['authServerUrl']) || empty($keycloakConfig['realm']) || empty($keycloakConfig['clientId']) || empty($keycloakConfig['clientSecret']) || empty($keycloakConfig['redirectUri'])) { + return ['errors' => 'Keycloak not configured']; + } + + $provider = new Keycloak($keycloakConfig); + + try { + $token = $provider->getAccessToken('authorization_code', ['code' => $args['code']]); + } catch (\Exception $e) { + return ['errors' => 'Authentication Failed']; + } + + try { + // We got an access token, let's now get the user's details + $user = $provider->getResourceOwner($token); + + $login = $user->getId(); + $keycloakAccessToken = $token->getToken(); + + $userMaarch = UserModel::getByLogin(['login' => $login, 'select' => ['id', 'external_id']]); + + if (empty($userMaarch)) { + return ['errors' => 'Authentication Failed']; + } + + $userMaarch['external_id'] = json_decode($userMaarch['external_id'], true); + $userMaarch['external_id']['keycloakAccessToken'] = $keycloakAccessToken; + $userMaarch['external_id'] = json_encode($userMaarch['external_id']); + + UserModel::updateExternalId(['id' => $userMaarch['id'], 'externalId' => $userMaarch['external_id']]); + + return ['login' => $login]; + } catch (\Exception $e) { + return ['errors' => 'Authentication Failed']; + } + } + + private static function keycloakDisconnection() + { + $keycloakConfig = CoreConfigModel::getKeycloakConfiguration(); + + $provider = new Keycloak($keycloakConfig); + + $externalId = UserModel::getById(['id' => $GLOBALS['id'], 'select' => ['external_id']]); + $externalId = json_decode($externalId['external_id'], true); + $accessToken = $externalId['keycloakAccessToken']; + unset($externalId['keycloakAccessToken']); + UserModel::update([ + 'set' => ['external_id' => json_encode($externalId)], + 'where' => ['id = ?'], + 'data' => [$GLOBALS['id']] + ]); + + $url = $provider->getLogoutUrl(['client_id' => $keycloakConfig['clientId'], 'refresh_token' => $accessToken]); + + return ['logoutUrl' => $url]; } public function getRefreshedToken(Request $request, Response $response) diff --git a/src/core/models/CoreConfigModel.php b/src/core/models/CoreConfigModel.php index fca2e40f58b5c4861aea9af1d229edc9662456a4..d8e16c0d4f4bef6c9f343d58f0d7cadcba794819 100755 --- a/src/core/models/CoreConfigModel.php +++ b/src/core/models/CoreConfigModel.php @@ -335,6 +335,17 @@ class CoreConfigModel $keycloakConfig['encryptionAlgorithm'] = (string)$loadedXml->ENCRYPTION_ALGORITHM; $keycloakConfig['encryptionKeyPath'] = (string)$loadedXml->ENCRYPTION_KEY_PATH; $keycloakConfig['encryptionKey'] = (string)$loadedXml->ENCRYPTION_KEY; + $keycloakConfig['scope'] = (string)$loadedXml->SCOPE; + + if (empty($keycloakConfig['encryptionAlgorithm'])) { + $keycloakConfig['encryptionAlgorithm'] = null; + } + if (empty($keycloakConfig['encryptionKeyPath'])) { + $keycloakConfig['encryptionKeyPath'] = null; + } + if (empty($keycloakConfig['encryptionKey'])) { + $keycloakConfig['encryptionKey'] = null; + } } } diff --git a/src/frontend/app/login/login.component.html b/src/frontend/app/login/login.component.html index 1cd2f086472b5a1a341f210686e9c48c8e459e0f..7b35d8fe65483647110e0d2e868625a28f5920f1 100644 --- a/src/frontend/app/login/login.component.html +++ b/src/frontend/app/login/login.component.html @@ -6,11 +6,11 @@ <div style="color: white;font-size: 14px;" [innerHTML]="loginMessage | safeHtml"></div> <p style="color: white;font-size: 14px;font-weight: bold;">{{applicationName}}</p> <div style="padding-left: 30px;padding-right: 30px;"> - <mat-form-field *ngIf="['cas'].indexOf(authService.authMode) === -1" class="input-row login" appearance="outline" style="padding-bottom: 0px;"> + <mat-form-field *ngIf="['cas', 'keycloak'].indexOf(authService.authMode) === -1" class="input-row login" appearance="outline" style="padding-bottom: 0px;"> <input id="login" name="login" matInput [placeholder]="this.translate.instant('lang.id')" formControlName="login" type="text"> </mat-form-field> - <mat-form-field *ngIf="['cas'].indexOf(authService.authMode) === -1" class="input-row" appearance="outline"> + <mat-form-field *ngIf="['cas', 'keycloak'].indexOf(authService.authMode) === -1" class="input-row" appearance="outline"> <input id="password" name="password" matInput [placeholder]="this.translate.instant('lang.password')" type="password" formControlName="password"> <mat-hint align="end" *ngIf="authService.authMode === 'standard'"><a @@ -19,7 +19,7 @@ class="infoLogin">{{'lang.' + authService.authMode + 'Enabled' | translate}}</span></mat-hint> </mat-form-field> </div> - <div *ngIf="['cas'].indexOf(authService.authMode) > -1" class="alert-message alert-message-info" role="alert" style="max-width: 100%;"> + <div *ngIf="['cas', 'keycloak'].indexOf(authService.authMode) > -1" class="alert-message alert-message-info" role="alert" style="max-width: 100%;"> {{'lang.' + authService.authMode + 'Enabled' | translate}} </div> <button id="submit" type="submit" mat-stroked-button [disabled]="loginForm.invalid || loading" diff --git a/src/frontend/app/login/login.component.ts b/src/frontend/app/login/login.component.ts index 7ef3a4fee88a6aefccaa65f39447140d5854d708..a2883d2da0b060a40ce71f4110e5a136c230cb2d 100644 --- a/src/frontend/app/login/login.component.ts +++ b/src/frontend/app/login/login.component.ts @@ -120,6 +120,13 @@ export class LoginComponent implements OnInit { this.authService.authMode = data.authMode; this.authService.authUri = data.authUri; + if (this.authService.authMode === 'keycloak') { + const keycloakState = this.localStorage.get('keycloakState'); + if (keycloakState === null || keycloakState === 'null') { + this.localStorage.save('keycloakState', data.keycloakState); + } + } + this.initConnection(); }), finalize(() => this.showForm = true), @@ -146,9 +153,25 @@ export class LoginComponent implements OnInit { if (['cas', 'keycloak'].indexOf(this.authService.authMode) > -1) { this.loginForm.disable(); this.loginForm.setValidators(null); - const regex = /ticket=[.]*/g; - if (window.location.search.match(regex) !== null) { + const regexCas = /ticket=[.]*/g; + const regexKeycloak = /code=[.]*/g; + if (window.location.search.match(regexCas) !== null || window.location.search.match(regexKeycloak) !== null) { const ssoToken = window.location.search.substring(1, window.location.search.length); + + const regexKeycloakState = /state=[.]*/g; + if (ssoToken.match(regexKeycloakState) !== null) { + const params = new URLSearchParams(window.location.search.substring(1)); + const keycloakState = this.localStorage.get('keycloakState'); + const paramState = params.get('state'); + + this.localStorage.save('keycloakState', null); + + if (keycloakState !== paramState && keycloakState !== null) { + window.location.href = this.authService.authUri; + return; + } + } + window.history.replaceState({}, document.title, window.location.pathname + window.location.hash); this.onSubmit(`?${ssoToken}`); } else { diff --git a/src/frontend/service/auth.service.ts b/src/frontend/service/auth.service.ts index 251fc73f2442c0e34994b6706639b7278312bf25..d980b410e40914e7ecb2926b077b5da6dc4860b6 100644 --- a/src/frontend/service/auth.service.ts +++ b/src/frontend/service/auth.service.ts @@ -102,7 +102,7 @@ export class AuthService { this.http.get('../rest/authenticate/logout').pipe( tap(async (data: any) => { this.redirectAfterLogout(cleanUrl); - window.location.href = data.logoutUrl + '?service=' + encodeURI(data.redirectUrl); + window.location.href = data.logoutUrl; }) ).subscribe(); }