From ad6c73e23c250abf777065f05b1eff7b6fe2845c Mon Sep 17 00:00:00 2001 From: Pauline Date: Mon, 5 Aug 2024 23:18:26 +0100 Subject: [PATCH 01/22] Update front end to use auth code login --- src/main/webapp/app/home/home.component.ts | 52 +++++++------------ .../app/shared/auth/auth-oauth2.service.ts | 48 +++++++---------- .../app/shared/auth/principal.service.ts | 1 - .../app/shared/login/login.component.ts | 28 +--------- .../webapp/app/shared/login/login.service.ts | 32 ++++++------ 5 files changed, 55 insertions(+), 106 deletions(-) diff --git a/src/main/webapp/app/home/home.component.ts b/src/main/webapp/app/home/home.component.ts index d0e01f247..13c1a9734 100644 --- a/src/main/webapp/app/home/home.component.ts +++ b/src/main/webapp/app/home/home.component.ts @@ -1,17 +1,15 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { ActivatedRoute, Router } from '@angular/router'; +import { first } from 'rxjs/operators'; import { - LoginModalService, ProjectService, Principal, Project, OrganizationService, + LoginService, } from '../shared'; -import {Observable, of, Subscription} from "rxjs"; -import { EventManager } from "../shared/util/event-manager.service"; -import { switchMap } from "rxjs/operators"; -import {SessionService} from "../shared/session/session.service"; -import {environment} from "../../environments/environment"; +import { Subscription } from "rxjs"; @Component({ selector: 'jhi-home', @@ -29,41 +27,31 @@ export class HomeComponent { private loginUrl = 'api/redirect/login'; constructor( - public principal: Principal, - private loginModalService: LoginModalService, - public projectService: ProjectService, - public organizationService: OrganizationService, + public principal: Principal, + public projectService: ProjectService, + public organizationService: OrganizationService, + private route: ActivatedRoute, + private loginService: LoginService, ) { this.subscriptions = new Subscription(); } - // ngOnInit() { - // this.loadRelevantProjects(); - // } - // - // ngOnDestroy() { - // this.subscriptions.unsubscribe(); - // } - // - // private loadRelevantProjects() { - // this.subscriptions.add(this.principal.account$ - // .pipe( - // switchMap(account => { - // if (account) { - // return this.userService.findProject(account.login); - // } else { - // return of([]); - // } - // }) - // ) - // .subscribe(projects => this.projects = projects)); - // } + ngOnInit() { + this.subscriptions.add(this.route.queryParams.subscribe((params) => { + const token = params['access_token']; + if (token) this.loginService.login(token).pipe(first()).toPromise() + })); + } + + ngOnDestroy() { + this.subscriptions.unsubscribe(); + } trackId(index: number, item: Project) { return item.projectName; } login() { - window.location.href = this.loginUrl + window.location.href = this.loginUrl } } diff --git a/src/main/webapp/app/shared/auth/auth-oauth2.service.ts b/src/main/webapp/app/shared/auth/auth-oauth2.service.ts index 96f3d0e4f..d8b8e9d50 100644 --- a/src/main/webapp/app/shared/auth/auth-oauth2.service.ts +++ b/src/main/webapp/app/shared/auth/auth-oauth2.service.ts @@ -2,48 +2,36 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; -import { map, switchMap } from "rxjs/operators"; -import {SessionService} from "../session/session.service"; -import {environment} from "../../../environments/environment"; +import { map } from 'rxjs/operators'; +import { SessionService } from '../session/session.service'; @Injectable({ providedIn: 'root' }) export class AuthServerProvider { - logoutUrl; constructor( - private http: HttpClient, - private sessionService: SessionService, + private http: HttpClient, + private sessionService: SessionService ) { - sessionService.logoutUrl$.subscribe( - url => this.logoutUrl = url - ) + sessionService.logoutUrl$.subscribe((url) => (this.logoutUrl = url)); } - login(credentials): Observable { - const body = new HttpParams() - .append('client_id', 'ManagementPortalapp') - .append('username', credentials.username) - .append('password', credentials.password) - .append('grant_type', 'password'); - const headers = new HttpHeaders() - .append('Content-Type', 'application/x-www-form-urlencoded') - .append('Accept', 'application/json'); - - return this.http.post('oauth/token', body, {headers, observe: 'body'}, ) - .pipe( - switchMap((tokenData: TokenData) => { - const authHeaders = new HttpHeaders() - .append('Authorization', 'Bearer ' + tokenData.access_token); - return this.http.post('api/login', null, { - headers: authHeaders, observe: 'body', withCredentials: true - }); - }), - ); + login(accessToken: string): Observable { + const authHeaders = new HttpHeaders().append( + 'Authorization', + 'Bearer ' + accessToken, + ); + return this.http.post('api/login', null, { + headers: authHeaders, + observe: 'body', + withCredentials: true, + }) } logout() { - window.location.href = this.logoutUrl + `&return_to=${window.location.href}`; + return this.http + .post('api/logout', { observe: 'body' }) + .pipe(map(() => {})); } } diff --git a/src/main/webapp/app/shared/auth/principal.service.ts b/src/main/webapp/app/shared/auth/principal.service.ts index 25d2666fb..4b6d379f2 100644 --- a/src/main/webapp/app/shared/auth/principal.service.ts +++ b/src/main/webapp/app/shared/auth/principal.service.ts @@ -17,7 +17,6 @@ export class Principal { // do not emit multiple duplicate values distinctUntilChanged((a, b) => a === b), ); - this.reset().subscribe(); } /** diff --git a/src/main/webapp/app/shared/login/login.component.ts b/src/main/webapp/app/shared/login/login.component.ts index d91a3a292..26985ec9d 100644 --- a/src/main/webapp/app/shared/login/login.component.ts +++ b/src/main/webapp/app/shared/login/login.component.ts @@ -45,33 +45,7 @@ export class JhiLoginModalComponent implements AfterViewInit { this.activeModal.dismiss('cancel'); } - login() { - this.loginService.login({ - username: this.username, - password: this.password, - rememberMe: this.rememberMe, - }).pipe(first()).toPromise().then(() => { - this.authenticationError = false; - this.activeModal.dismiss('login success'); - if (this.router.url === '/register' || (/activate/.test(this.router.url)) || - this.router.url === '/finishReset' || this.router.url === '/requestReset') { - return this.router.navigate(['']); - } - - this.eventManager.broadcast({ - name: 'authenticationSuccess', - content: 'Sending Authentication Success', - }); - - return this.authService.redirectBeforeUnauthenticated(); - }).catch(() => { - this.authenticationError = true; - }).then((isRedirected) => { - if (!isRedirected) { - return this.router.navigate(['/']); - } - }); - } + login() {} register() { this.activeModal.dismiss('to state register'); diff --git a/src/main/webapp/app/shared/login/login.service.ts b/src/main/webapp/app/shared/login/login.service.ts index e833459e5..7bd2e6682 100644 --- a/src/main/webapp/app/shared/login/login.service.ts +++ b/src/main/webapp/app/shared/login/login.service.ts @@ -16,24 +16,24 @@ export class LoginService { ) { } - login(credentials): Observable { - return this.authServerProvider.login(credentials).pipe( - tap( - (account) => { - this.principal.authenticate(account); - // After the login the language will be changed to - // the language selected by the user during his registration - if (account && account.langKey) { - this.translateService.use(account.langKey); - } - }, - () => this.logout() - ), - ); - } + login(accessToken: string): Observable { + return this.authServerProvider.login(accessToken).pipe( + tap( + (account) => { + this.principal.authenticate(account); + // After the login the language will be changed to + // the language selected by the user during his registration + if (account && account.langKey) { + this.translateService.use(account.langKey); + } + }, + () => this.logout() + ), + ); + } logout() { - this.authServerProvider.logout(); + this.authServerProvider.logout().subscribe(); this.principal.authenticate(null); } } From 8c9fec3da23c13e19c601762487acb4db53e7427 Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 6 Aug 2024 00:12:39 +0100 Subject: [PATCH 02/22] Add login endpoint to allow auth code grant login --- .../management/web/rest/LoginEndpoint.kt | 88 +++++++++++++++++-- 1 file changed, 82 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt index 12017d8ad..8e2b19d29 100644 --- a/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt +++ b/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt @@ -1,36 +1,112 @@ package org.radarbase.management.web.rest +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.jsonPrimitive +import org.radarbase.auth.exception.IdpException import org.radarbase.management.config.ManagementPortalProperties import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import org.springframework.web.servlet.view.RedirectView +import java.time.Duration +import java.time.Instant @RestController @RequestMapping("/api") class LoginEndpoint @Autowired constructor( - @Autowired private val managementPortalProperties: ManagementPortalProperties, + private val managementPortalProperties: ManagementPortalProperties, ) { + private val httpClient = + HttpClient(CIO) { + install(HttpTimeout) { + connectTimeoutMillis = Duration.ofSeconds(10).toMillis() + socketTimeoutMillis = Duration.ofSeconds(10).toMillis() + requestTimeoutMillis = Duration.ofSeconds(300).toMillis() + } + install(ContentNegotiation) { + json(Json { ignoreUnknownKeys = true }) + } + } + @GetMapping("/redirect/login") - fun loginRedirect(): RedirectView { + suspend fun loginRedirect( + @RequestParam(required = false) code: String?, + ): RedirectView { val redirectView = RedirectView() - redirectView.url = managementPortalProperties.identityServer.loginUrl + - "/login?return_to=" + managementPortalProperties.common.managementPortalBaseUrl + val config = managementPortalProperties + val mpUrl = config.common.baseUrl + + if (code == null) { + redirectView.url = buildAuthUrl(config, mpUrl) + } else { + val accessToken = fetchAccessToken(code, config) + redirectView.url = "$mpUrl/#/?access_token=$accessToken" + } return redirectView } @GetMapping("/redirect/account") fun settingsRedirect(): RedirectView { val redirectView = RedirectView() - redirectView.url = managementPortalProperties.identityServer.loginUrl + "/settings" + redirectView.url = "${managementPortalProperties.identityServer.loginUrl}/settings" return redirectView } + private fun buildAuthUrl(config: ManagementPortalProperties, mpUrl: String): String { + return "${config.authServer.serverUrl}/oauth2/auth?" + + "client_id=${config.frontend.clientId}&" + + "response_type=code&" + + "state=${Instant.now()}&" + + "audience=res_ManagementPortal&" + + "scope=offline&" + + "redirect_uri=$mpUrl/api/redirect/login" + } + + private suspend fun fetchAccessToken( + code: String, + config: ManagementPortalProperties, + ): String { + val tokenUrl = "${config.authServer.serverUrl}/oauth2/token" + val response = + httpClient.post(tokenUrl) { + contentType(ContentType.Application.FormUrlEncoded) + accept(ContentType.Application.Json) + setBody( + Parameters + .build { + append("grant_type", "authorization_code") + append("code", code) + append("redirect_uri", "${config.common.baseUrl}/api/redirect/login") + append("client_id", config.frontend.clientId) + }.formUrlEncode(), + ) + } + + if (response.status.isSuccess()) { + val responseMap = response.body>() + return responseMap["access_token"]?.jsonPrimitive?.content + ?: throw IdpException("Access token not found in response") + } else { + throw IdpException("Unable to get access token") + } + } + companion object { - private val logger = LoggerFactory.getLogger(TokenKeyEndpoint::class.java) + private val logger = LoggerFactory.getLogger(LoginEndpoint::class.java) } } From c09291cbde7dd9a7542aa1f57cb326e7a1e23c4b Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 6 Aug 2024 00:13:06 +0100 Subject: [PATCH 03/22] Add token validator updates to support hydra tokens --- .../config/ManagementPortalProperties.java | 28 +++++++++++++++++++ .../config/OAuth2ServerConfiguration.kt | 1 + ...ManagementPortalJwtAccessTokenConverter.kt | 6 +++- .../ManagementPortalOauthKeyStoreHandler.kt | 9 +++++- 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/radarbase/management/config/ManagementPortalProperties.java b/src/main/java/org/radarbase/management/config/ManagementPortalProperties.java index 520bd1d69..8704edb96 100644 --- a/src/main/java/org/radarbase/management/config/ManagementPortalProperties.java +++ b/src/main/java/org/radarbase/management/config/ManagementPortalProperties.java @@ -12,6 +12,8 @@ public class ManagementPortalProperties { private final IdentityServer identityServer = new IdentityServer(); + private final AuthServer authServer = new AuthServer(); + private final Mail mail = new Mail(); private final Frontend frontend = new Frontend(); @@ -34,6 +36,10 @@ public IdentityServer getIdentityServer() { return identityServer; } + public AuthServer getAuthServer() { + return authServer; + } + public ManagementPortalProperties.Mail getMail() { return mail; } @@ -324,6 +330,28 @@ public void setLoginUrl(String loginUrl) { } } + public class AuthServer { + private String serverUrl = null; + private String serverAdminUrl = null; + + public String getServerUrl() { + return serverUrl; + } + + public void setServerUrl(String serverUrl) { + this.serverUrl = serverUrl; + } + + public String getServerAdminUrl() { + return serverAdminUrl; + } + + public void setServerAdminUrl(String serverAdminUrl) { + this.serverAdminUrl = serverAdminUrl; + } + } + + public static class CatalogueServer { private boolean enableAutoImport = false; diff --git a/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt b/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt index 2586beaf3..e5a7a22bf 100644 --- a/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt +++ b/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt @@ -242,6 +242,7 @@ class OAuth2ServerConfiguration( fun accessTokenConverter(): ManagementPortalJwtAccessTokenConverter { logger.debug("loading token converter from keystore configurations") return ManagementPortalJwtAccessTokenConverter( + keyStoreHandler.tokenValidator, keyStoreHandler.algorithmForSigning, keyStoreHandler.verifiers, keyStoreHandler.refreshTokenVerifiers diff --git a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt index a57826d0a..06bd7aa93 100644 --- a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt +++ b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt @@ -8,6 +8,7 @@ import com.auth0.jwt.exceptions.SignatureVerificationException import com.fasterxml.jackson.core.JsonProcessingException import com.fasterxml.jackson.databind.ObjectMapper import org.radarbase.management.security.jwt.ManagementPortalJwtAccessTokenConverter +import org.radarbase.auth.authentication.TokenValidator import org.slf4j.LoggerFactory import org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken @@ -33,6 +34,7 @@ import java.util.stream.Stream * are significantly smaller than RSA signatures. */ open class ManagementPortalJwtAccessTokenConverter( + validator: TokenValidator, algorithm: Algorithm, verifiers: MutableList, private val refreshTokenVerifiers: List @@ -59,6 +61,7 @@ open class ManagementPortalJwtAccessTokenConverter( field = jwtClaimsSetVerifier } private var algorithm: Algorithm? = null + private var validator: TokenValidator private val verifiers: MutableList /** @@ -72,6 +75,7 @@ open class ManagementPortalJwtAccessTokenConverter( accessToken.setIncludeGrantType(true) tokenConverter = accessToken this.verifiers = verifiers + this.validator = validator setAlgorithm(algorithm) } @@ -229,7 +233,7 @@ open class ManagementPortalJwtAccessTokenConverter( } for (verifier in verifierToUse) { try { - verifier.verify(token) + validator.validateBlocking(token) return claims } catch (sve: SignatureVerificationException) { logger.warn("Client presented a token with an incorrect signature") diff --git a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt index 752a295f1..a58026e35 100644 --- a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt +++ b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt @@ -52,6 +52,7 @@ class ManagementPortalOauthKeyStoreHandler @Autowired constructor( private val oauthConfig: Oauth private val verifierPublicKeyAliasList: List private val managementPortalBaseUrl: String + private val authServerUrl: String val verifiers: MutableList val refreshTokenVerifiers: MutableList @@ -79,6 +80,8 @@ class ManagementPortalOauthKeyStoreHandler @Autowired constructor( // No need to check audience with a refresh token: it can be used // to refresh tokens intended for other resources. refreshTokenVerifiers = algorithms.map { algo: Algorithm -> JWT.require(algo).build() }.toMutableList() + authServerUrl = managementPortalProperties.authServer.serverAdminUrl + tokenValidator.refresh() } @Nonnull @@ -228,7 +231,11 @@ class ManagementPortalOauthKeyStoreHandler @Autowired constructor( RES_MANAGEMENT_PORTAL, JwkAlgorithmParser() ), - KratosTokenVerifierLoader(managementPortalProperties.identityServer.publicUrl(), requireAal2 = managementPortalProperties.oauth.requireAal2), + JwksTokenVerifierLoader( + authServerUrl + "/admin/keys/hydra.jwt.access-token", + RES_MANAGEMENT_PORTAL, + JwkAlgorithmParser() + ), ) return TokenValidator(loaderList) } From 4820edc7ebf0dbcf20596dbdf3ec01f052382643 Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 6 Aug 2024 11:00:36 +0100 Subject: [PATCH 04/22] Add auth server config to application properties --- src/main/resources/config/application-dev.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/resources/config/application-dev.yml b/src/main/resources/config/application-dev.yml index 87e51ec0d..b14bc7901 100644 --- a/src/main/resources/config/application-dev.yml +++ b/src/main/resources/config/application-dev.yml @@ -116,9 +116,14 @@ managementportal: # The line below can be uncommented to add some hidden fields for UI testing #hiddenSubjectFields: [person_name, date_of_birth, group] identityServer: + adminEmail: admin-email-here@gmail.com serverUrl: http://localhost:4433 - serverAdminUrl: http://localhost:4434 + serverAdminUrl: http://kratos-admin loginUrl: http://localhost:3000 + authServer: + serverUrl: http://localhost:4444 + serverAdminUrl: http://localhost:4445 + # =================================================================== # JHipster specific properties From 6beedc45c56bf27086a549811a80a87d1d6da1c8 Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 6 Aug 2024 16:27:54 +0100 Subject: [PATCH 05/22] Fix access token converter --- ...ManagementPortalJwtAccessTokenConverter.kt | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt index 06bd7aa93..88962f0a2 100644 --- a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt +++ b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt @@ -25,6 +25,7 @@ import java.nio.charset.StandardCharsets import java.time.Instant import java.util.* import java.util.stream.Stream +import org.radarbase.auth.exception.TokenValidationException /** * Implementation of [JwtAccessTokenConverter] for the RADAR-base ManagementPortal platform. @@ -231,17 +232,13 @@ open class ManagementPortalJwtAccessTokenConverter( } catch (ex: JsonProcessingException) { throw InvalidTokenException("Invalid token", ex) } - for (verifier in verifierToUse) { - try { - validator.validateBlocking(token) - return claims - } catch (sve: SignatureVerificationException) { - logger.warn("Client presented a token with an incorrect signature") - } catch (ex: JWTVerificationException) { - logger.debug( - "Verifier {} with implementation {} did not accept token: {}", - verifier, verifier.javaClass, ex.message - ) + try { + validator.validateBlocking(token) + Companion.logger.debug("Using token from header") + return claims + } catch (ex: TokenValidationException) { + ex.message?.let { + Companion.logger.info("Failed to validate token from header: {}", it) } } throw InvalidTokenException("No registered validator could authenticate this token") From d0d61aaa26b017d204bb8c2267b16bf26d4d4039 Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 13 Aug 2024 20:41:22 +0100 Subject: [PATCH 06/22] Fix application properties --- src/main/resources/config/application-prod.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/resources/config/application-prod.yml b/src/main/resources/config/application-prod.yml index 5fa817e8b..83746c671 100644 --- a/src/main/resources/config/application-prod.yml +++ b/src/main/resources/config/application-prod.yml @@ -80,7 +80,7 @@ server: # =================================================================== managementportal: common: - baseUrl: http://my-server-url-to-change-here # Modify according to your server's URL + baseUrl: http://localhost:8080/managementportal # Modify according to your server's URL managementPortalBaseUrl: http://localhost:8080/managementportal privacyPolicyUrl: http://info.thehyve.nl/radar-cns-privacy-policy adminPassword: @@ -101,9 +101,12 @@ managementportal: enableAutoImport: false identityServer: adminEmail: bdegraaf1234@gmail.com - serverUrl: https://radar-k3s-test.thehyve.net/kratos + serverUrl: http://localhost:4433 serverAdminUrl: http://kratos-admin loginUrl: http://localhost:3000 + authServer: + serverUrl: http://localhost:4444 + serverAdminUrl: http://localhost:4445 # =================================================================== # JHipster specific properties From 5234fd2383439629d68b8a72b37808979f890537 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 14 Aug 2024 12:30:57 +0100 Subject: [PATCH 07/22] Remove unused KratosTokenVerifier and Oauth2LoginUiWebConfig --- .../auth/kratos/KratosTokenVerifier.kt | 33 --- .../auth/kratos/KratosTokenVerifierLoader.kt | 14 -- .../config/OAuth2LoginUiWebConfig.kt | 218 ------------------ 3 files changed, 265 deletions(-) delete mode 100644 radar-auth/src/main/java/org/radarbase/auth/kratos/KratosTokenVerifier.kt delete mode 100644 radar-auth/src/main/java/org/radarbase/auth/kratos/KratosTokenVerifierLoader.kt delete mode 100644 src/main/java/org/radarbase/management/config/OAuth2LoginUiWebConfig.kt diff --git a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosTokenVerifier.kt b/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosTokenVerifier.kt deleted file mode 100644 index 321ec1a92..000000000 --- a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosTokenVerifier.kt +++ /dev/null @@ -1,33 +0,0 @@ -package org.radarbase.auth.kratos -import org.radarbase.auth.authentication.TokenVerifier -import org.radarbase.auth.exception.IdpException -import org.radarbase.auth.exception.InsufficientAuthenticationLevelException -import org.radarbase.auth.token.RadarToken -import org.slf4j.LoggerFactory - -//TODO Better error screen for no AAL2 -class KratosTokenVerifier(private val sessionService: SessionService, private val requireAal2: Boolean) : TokenVerifier { - @Throws(IdpException::class) - override suspend fun verify(token: String): RadarToken = try { - val kratosSession = sessionService.getSession(token) - - val radarToken = kratosSession.toDataRadarToken() - if (radarToken.authenticatorAssuranceLevel != RadarToken.AuthenticatorAssuranceLevel.aal2 && requireAal2) - { - val msg = "found a token of with aal: ${radarToken.authenticatorAssuranceLevel}, which is insufficient for this" + - " action" - throw InsufficientAuthenticationLevelException(msg) - } - radarToken - } catch (ex: InsufficientAuthenticationLevelException) { - throw ex - } catch (ex: Throwable) { - throw IdpException("could not verify token", ex) - } - - override fun toString(): String = "org.radarbase.auth.kratos.KratosTokenVerifier" - - companion object { - private val logger = LoggerFactory.getLogger(KratosTokenVerifier::class.java) - } -} diff --git a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosTokenVerifierLoader.kt b/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosTokenVerifierLoader.kt deleted file mode 100644 index 7c567cc3a..000000000 --- a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosTokenVerifierLoader.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.radarbase.auth.kratos -import org.radarbase.auth.authentication.TokenVerifier -import org.radarbase.auth.authentication.TokenVerifierLoader - -class KratosTokenVerifierLoader(private val serverUrl: String, private val requireAal2: Boolean) : TokenVerifierLoader { - - override suspend fun fetch(): List { - return listOf( - KratosTokenVerifier(SessionService(serverUrl), requireAal2) - ) - } - - override fun toString(): String = "KratosTokenKeyAlgorithmKeyLoader" -} diff --git a/src/main/java/org/radarbase/management/config/OAuth2LoginUiWebConfig.kt b/src/main/java/org/radarbase/management/config/OAuth2LoginUiWebConfig.kt deleted file mode 100644 index fd88d3162..000000000 --- a/src/main/java/org/radarbase/management/config/OAuth2LoginUiWebConfig.kt +++ /dev/null @@ -1,218 +0,0 @@ -package org.radarbase.management.config - -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.http.HttpHeaders -import org.springframework.http.HttpStatus -import org.springframework.http.MediaType -import org.springframework.http.ResponseEntity -import org.springframework.security.authentication.InsufficientAuthenticationException -import org.springframework.security.core.Authentication -import org.springframework.security.core.GrantedAuthority -import org.springframework.security.oauth2.common.OAuth2AccessToken -import org.springframework.security.oauth2.common.exceptions.OAuth2Exception -import org.springframework.security.oauth2.common.util.OAuth2Utils -import org.springframework.security.oauth2.provider.ClientDetailsService -import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint -import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory -import org.springframework.stereotype.Controller -import org.springframework.web.HttpRequestMethodNotSupportedException -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.SessionAttributes -import org.springframework.web.servlet.ModelAndView -import org.springframework.web.util.HtmlUtils -import java.net.URLEncoder -import java.security.Principal -import java.text.SimpleDateFormat -import java.util.* -import java.util.function.Function -import java.util.stream.Collectors -import java.util.stream.Stream -import javax.servlet.RequestDispatcher -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.set - -/** - * Created by dverbeec on 6/07/2017. - */ -@Controller -@SessionAttributes("authorizationRequest") -class OAuth2LoginUiWebConfig( - @Autowired private val tokenEndPoint: TokenEndpoint, - @Autowired private val managementPortalProperties: ManagementPortalProperties -) { - - @Autowired - private val clientDetailsService: ClientDetailsService? = null - - @RequestMapping("/oauth2/authorize") - fun redirect_authorize(request: HttpServletRequest): String { - val returnString = URLEncoder.encode(request.requestURL.toString().replace("oauth2", "oauth") + "?" + request.parameterMap.map{ param -> param.key + "=" + param.value.first()}.joinToString("&"), "UTF-8") - val mpUrl = managementPortalProperties.common.baseUrl - return "redirect:$mpUrl/kratos-ui/login?return_to=$returnString" - } - - @PostMapping( - "/oauth2/token", - consumes = [MediaType.APPLICATION_FORM_URLENCODED_VALUE], - produces = [MediaType.APPLICATION_FORM_URLENCODED_VALUE] - ) - fun redirect_token(@RequestParam parameters: Map, request: HttpServletRequest, response: HttpServletResponse) { - var dispatcher: RequestDispatcher = request.servletContext.getRequestDispatcher("/oauth/token/") - dispatcher.forward(request, response) - } - - @PostMapping(value = ["/oauth/token"], - consumes = [MediaType.APPLICATION_FORM_URLENCODED_VALUE] - ) - @Throws( - HttpRequestMethodNotSupportedException::class - ) - fun postAccessToken(@RequestParam parameters: Map, principal: Principal?): - ResponseEntity { - if (principal !is Authentication) { - throw InsufficientAuthenticationException( - "There is no client authentication. Try adding an appropriate authentication filter." - ) - } - - val grant_type = parameters.get("grant_type") - logger.debug("Token request of grant type $grant_type received") - - val clientId: String = parameters.get("client_id") ?: principal.name - var radarPrincipal = RadarPrincipal(clientId, principal) - - val token2 = this.tokenEndPoint.postAccessToken(radarPrincipal, parameters) - return getResponse(token2.body) - } - - fun getResponse(accessToken: OAuth2AccessToken): ResponseEntity { - val headers = HttpHeaders() - headers["Cache-Control"] = "no-store" - headers["Pragma"] = "no-cache" - headers["Content-Type"] = "application/json" - return ResponseEntity(accessToken, headers, HttpStatus.OK) - } - - /** - * Login form for OAuth2 auhorization flows. - * @param request the servlet request - * @param response the servlet response - * @return a ModelAndView to render the form - */ - @RequestMapping("/login") - fun getLogin(request: HttpServletRequest, response: HttpServletResponse?): ModelAndView { - val model = TreeMap() - if (request.parameterMap.containsKey("error")) { - model["loginError"] = true - } - return ModelAndView("login", model) - } - - /** - * Form for a client to confirm authorizing an OAuth client access to the requested resources. - * @param request the servlet request - * @param response the servlet response - * @return a ModelAndView to render the form - */ - @RequestMapping("/oauth/confirm_access") - fun getAccessConfirmation( - request: HttpServletRequest, - response: HttpServletResponse? - ): ModelAndView { - val params = request.parameterMap - val authorizationParameters = Stream.of( - OAuth2Utils.CLIENT_ID, OAuth2Utils.REDIRECT_URI, OAuth2Utils.STATE, - OAuth2Utils.SCOPE, OAuth2Utils.RESPONSE_TYPE - ) - .filter { key: String -> params.containsKey(key) } - .collect(Collectors.toMap(Function.identity(), Function { p: String -> params[p]!![0] })) - val authorizationRequest = DefaultOAuth2RequestFactory( - clientDetailsService - ).createAuthorizationRequest(authorizationParameters) - val model = Collections.singletonMap( - "authorizationRequest", - authorizationRequest - ) - return ModelAndView("authorize", model) - } - - /** - * A page to render errors that arised during an OAuth flow. - * @param req the servlet request - * @return a ModelAndView to render the page - */ - @RequestMapping("/oauth/error") - fun handleOAuthClientError(req: HttpServletRequest): ModelAndView { - val model = TreeMap() - val error = req.getAttribute("error") - // The error summary may contain malicious user input, - // it needs to be escaped to prevent XSS - val errorParams: MutableMap = HashMap() - errorParams["date"] = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) - .format(Date()) - if (error is OAuth2Exception) { - val oauthError = error - errorParams["status"] = String.format("%d", oauthError.httpErrorCode) - errorParams["code"] = oauthError.oAuth2ErrorCode - errorParams["message"] = oauthError.message?.let { HtmlUtils.htmlEscape(it) } ?: "No error message found" - // transform the additionalInfo map to a comma seperated list of key: value pairs - if (oauthError.additionalInformation != null) { - errorParams["additionalInfo"] = HtmlUtils.htmlEscape( - oauthError.additionalInformation.entries.joinToString(", ") { entry -> entry.key + ": " + entry.value } - ) - } - } - // Copy non-empty entries to the model. Empty entries will not be present in the model, - // so the default value will be rendered in the view. - for ((key, value) in errorParams) { - if (value != "") { - model[key] = value - } - } - return ModelAndView("error", model) - } - - private class RadarPrincipal(private val name: String, private val auth: Authentication) : Principal, Authentication { - - override fun getName(): String { - return name - } - - override fun getAuthorities(): MutableCollection { - return auth.authorities - } - - override fun getCredentials(): Any { - return auth.credentials - } - - override fun getDetails(): Any { - return auth.details - } - - override fun getPrincipal(): Any { - return this - } - - override fun isAuthenticated(): Boolean { - return auth.isAuthenticated - } - - override fun setAuthenticated(isAuthenticated: Boolean) { - auth.isAuthenticated = isAuthenticated - } - - } - - companion object { - private val logger = LoggerFactory.getLogger( - OAuth2LoginUiWebConfig::class.java - ) - } -} From ea477757db207219d5bb4fcb7fe2f94557a6dd6f Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 14 Aug 2024 12:32:00 +0100 Subject: [PATCH 08/22] Update setting of hydra token verifier loader --- .../jwt/ManagementPortalOauthKeyStoreHandler.kt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt index a58026e35..493cd9af3 100644 --- a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt +++ b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt @@ -7,7 +7,6 @@ import org.radarbase.auth.authentication.TokenValidator import org.radarbase.auth.jwks.JsonWebKeySet import org.radarbase.auth.jwks.JwkAlgorithmParser import org.radarbase.auth.jwks.JwksTokenVerifierLoader -import org.radarbase.auth.kratos.KratosTokenVerifierLoader import org.radarbase.management.config.ManagementPortalProperties import org.radarbase.management.config.ManagementPortalProperties.Oauth import org.radarbase.management.security.jwt.ManagementPortalJwtAccessTokenConverter.Companion.RES_MANAGEMENT_PORTAL @@ -52,7 +51,6 @@ class ManagementPortalOauthKeyStoreHandler @Autowired constructor( private val oauthConfig: Oauth private val verifierPublicKeyAliasList: List private val managementPortalBaseUrl: String - private val authServerUrl: String val verifiers: MutableList val refreshTokenVerifiers: MutableList @@ -80,7 +78,6 @@ class ManagementPortalOauthKeyStoreHandler @Autowired constructor( // No need to check audience with a refresh token: it can be used // to refresh tokens intended for other resources. refreshTokenVerifiers = algorithms.map { algo: Algorithm -> JWT.require(algo).build() }.toMutableList() - authServerUrl = managementPortalProperties.authServer.serverAdminUrl tokenValidator.refresh() } @@ -231,11 +228,13 @@ class ManagementPortalOauthKeyStoreHandler @Autowired constructor( RES_MANAGEMENT_PORTAL, JwkAlgorithmParser() ), - JwksTokenVerifierLoader( - authServerUrl + "/admin/keys/hydra.jwt.access-token", - RES_MANAGEMENT_PORTAL, - JwkAlgorithmParser() - ), + managementPortalProperties.authServer.let { + JwksTokenVerifierLoader( + it.serverAdminUrl + "/admin/keys/hydra.jwt.access-token", + RES_MANAGEMENT_PORTAL, + JwkAlgorithmParser() + ) + }, ) return TokenValidator(loaderList) } From 1a9dacabd697fe87ff92e5d5e09b7c8b28e8ec00 Mon Sep 17 00:00:00 2001 From: yatharthranjan Date: Mon, 19 Aug 2024 15:00:51 +0100 Subject: [PATCH 09/22] add separate login url for hydra --- .../org/radarbase/management/web/rest/LoginEndpoint.kt | 7 +------ src/main/resources/config/application-dev.yml | 1 + src/main/resources/config/application-prod.yml | 5 +++-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt index 8e2b19d29..202e701ae 100644 --- a/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt +++ b/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt @@ -6,7 +6,6 @@ import io.ktor.client.engine.cio.* import io.ktor.client.plugins.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.request.* -import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.json.Json @@ -68,7 +67,7 @@ class LoginEndpoint } private fun buildAuthUrl(config: ManagementPortalProperties, mpUrl: String): String { - return "${config.authServer.serverUrl}/oauth2/auth?" + + return "${config.authServer.loginUrl}/oauth2/auth?" + "client_id=${config.frontend.clientId}&" + "response_type=code&" + "state=${Instant.now()}&" + @@ -105,8 +104,4 @@ class LoginEndpoint throw IdpException("Unable to get access token") } } - - companion object { - private val logger = LoggerFactory.getLogger(LoginEndpoint::class.java) - } } diff --git a/src/main/resources/config/application-dev.yml b/src/main/resources/config/application-dev.yml index b14bc7901..76a54b5b7 100644 --- a/src/main/resources/config/application-dev.yml +++ b/src/main/resources/config/application-dev.yml @@ -123,6 +123,7 @@ managementportal: authServer: serverUrl: http://localhost:4444 serverAdminUrl: http://localhost:4445 + loginUrl: http://localhost:4444 # =================================================================== diff --git a/src/main/resources/config/application-prod.yml b/src/main/resources/config/application-prod.yml index 83746c671..436de6e62 100644 --- a/src/main/resources/config/application-prod.yml +++ b/src/main/resources/config/application-prod.yml @@ -105,8 +105,9 @@ managementportal: serverAdminUrl: http://kratos-admin loginUrl: http://localhost:3000 authServer: - serverUrl: http://localhost:4444 - serverAdminUrl: http://localhost:4445 + serverUrl: http://hydra:4444 + serverAdminUrl: http://hydra:4445 + loginUrl: http://localhost:4444 # =================================================================== # JHipster specific properties From bc2b2f691700c43b97c32cff7639aa5b2b62505c Mon Sep 17 00:00:00 2001 From: Pauline Date: Mon, 19 Aug 2024 18:23:09 +0100 Subject: [PATCH 10/22] Update ManagementPortal propreties with new loginUrl property --- .../management/config/ManagementPortalProperties.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/org/radarbase/management/config/ManagementPortalProperties.java b/src/main/java/org/radarbase/management/config/ManagementPortalProperties.java index 8704edb96..8518f9554 100644 --- a/src/main/java/org/radarbase/management/config/ManagementPortalProperties.java +++ b/src/main/java/org/radarbase/management/config/ManagementPortalProperties.java @@ -333,6 +333,7 @@ public void setLoginUrl(String loginUrl) { public class AuthServer { private String serverUrl = null; private String serverAdminUrl = null; + private String loginUrl = null; public String getServerUrl() { return serverUrl; @@ -349,6 +350,14 @@ public String getServerAdminUrl() { public void setServerAdminUrl(String serverAdminUrl) { this.serverAdminUrl = serverAdminUrl; } + + public String getLoginUrl() { + return loginUrl; + } + + public void setLoginUrl(String loginUrl) { + this.loginUrl = loginUrl; + } } From 57eecfdafd7d14c44c4b014fb42156fdfedc1d86 Mon Sep 17 00:00:00 2001 From: yatharthranjan Date: Tue, 20 Aug 2024 12:39:42 +0100 Subject: [PATCH 11/22] intermediate removal of unneeded components - This disables the components, but these can be removed once tested. --- src/main/docker/managementportal.yml | 3 +- src/main/docker/ory_stack.yml | 2 +- .../config/OAuth2ServerConfiguration.kt | 139 ++++++++++++------ .../security/JwtAuthenticationFilter.kt | 31 ++++ ...ManagementPortalJwtAccessTokenConverter.kt | 28 ++-- .../ManagementPortalOauthKeyStoreHandler.kt | 4 +- .../management/service/MetaTokenService.kt | 4 +- .../management/service/OAuthClientService.kt | 2 +- .../decorator/ProjectMapperDecorator.kt | 12 +- .../management/web/rest/MetaTokenResource.kt | 4 +- .../web/rest/OAuthClientsResource.kt | 4 +- .../management/web/rest/TokenKeyEndpoint.kt | 2 +- 12 files changed, 157 insertions(+), 78 deletions(-) diff --git a/src/main/docker/managementportal.yml b/src/main/docker/managementportal.yml index b19a7a337..ad492395c 100644 --- a/src/main/docker/managementportal.yml +++ b/src/main/docker/managementportal.yml @@ -12,9 +12,10 @@ services: - MANAGEMENTPORTAL_FRONTEND_CLIENT_SECRET=secret - MANAGEMENTPORTAL_IDENTITYSERVER_ADMINEMAIL=admin-email-here@radar-base.net - MANAGEMENTPORTAL_IDENTITYSERVER_SERVERURL=http://kratos:4433 - - MANAGEMENTPORTAL_IDENTITYSERVER_LOGINURL=http://radar-self-enrolment-ui:3000 + - MANAGEMENTPORTAL_IDENTITYSERVER_LOGINURL=http://localhost:3000 - MANAGEMENTPORTAL_IDENTITYSERVER_SERVERADMINURL=http://kratos:4434 - MANAGEMENTPORTAL_AUTHSERVER_SERVERURL=http://hydra:4444 + - MANAGEMENTPORTAL_AUTHSERVER_LOGINURL=http://localhost:4444 - MANAGEMENTPORTAL_AUTHSERVER_SERVERADMINURL=http://hydra:4445 - JHIPSTER_SLEEP=10 # gives time for the database to boot before the application - JAVA_OPTS=-Xmx512m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 #enables remote debugging diff --git a/src/main/docker/ory_stack.yml b/src/main/docker/ory_stack.yml index c2f545d13..ba8eb92b4 100644 --- a/src/main/docker/ory_stack.yml +++ b/src/main/docker/ory_stack.yml @@ -5,7 +5,7 @@ volumes: services: radar-self-enrolment-ui: - image: ghcr.io/radar-base/radar-self-enrolment-ui:dev + image: ghcr.io/radar-base/radar-self-enrolment-ui:feat-consent-module environment: - ORY_SDK_URL=http://kratos:4433/ - HYDRA_ADMIN_URL=http://hydra:4445 diff --git a/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt b/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt index 8c00fa2b0..bce2afc2a 100644 --- a/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt +++ b/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt @@ -1,8 +1,13 @@ package org.radarbase.management.config +import com.auth0.jwt.JWT +import org.radarbase.auth.authentication.TokenValidator import java.util.* import javax.sql.DataSource import org.radarbase.auth.authorization.RoleAuthority +import org.radarbase.auth.jwks.JwkAlgorithmParser +import org.radarbase.auth.jwks.JwksTokenVerifierLoader +import org.radarbase.auth.jwt.JwtTokenVerifier import org.radarbase.management.repository.UserRepository import org.radarbase.management.security.ClaimsTokenEnhancer import org.radarbase.management.security.Http401UnauthorizedEntryPoint @@ -53,9 +58,8 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessHandl @Configuration class OAuth2ServerConfiguration( @Autowired private val dataSource: DataSource, - @Autowired private val passwordEncoder: PasswordEncoder + @Autowired private val passwordEncoder: PasswordEncoder, ) { - @Configuration @Order(-20) protected class LoginConfig( @@ -91,12 +95,25 @@ class OAuth2ServerConfiguration( class JwtAuthenticationFilterConfiguration( @Autowired private val authenticationManager: AuthenticationManager, @Autowired private val userRepository: UserRepository, - @Autowired private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler + @Autowired private val managementPortalProperties: ManagementPortalProperties, + //@Autowired private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler ) { + val tokenValidator: TokenValidator + /** Get the default token validator. */ + get() { + val loaderList = listOf( + JwksTokenVerifierLoader( + managementPortalProperties.authServer.serverAdminUrl + "/admin/keys/hydra.jwt.access-token", + ManagementPortalJwtAccessTokenConverter.RES_MANAGEMENT_PORTAL, + JwkAlgorithmParser() + ), + ) + return TokenValidator(loaderList) + } @Bean fun jwtAuthenticationFilter(): JwtAuthenticationFilter { return JwtAuthenticationFilter( - keyStoreHandler.tokenValidator, + tokenValidator, authenticationManager, userRepository, true @@ -114,17 +131,32 @@ class OAuth2ServerConfiguration( @Configuration @EnableResourceServer protected class ResourceServerConfiguration( - @Autowired private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler, - @Autowired private val tokenStore: TokenStore, + //@Autowired private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler, + //@Autowired private val tokenStore: TokenStore, + @Autowired private val managementPortalProperties: ManagementPortalProperties, @Autowired private val http401UnauthorizedEntryPoint: Http401UnauthorizedEntryPoint, @Autowired private val logoutSuccessHandler: LogoutSuccessHandler, @Autowired private val authenticationManager: AuthenticationManager, @Autowired private val userRepository: UserRepository ) : ResourceServerConfigurerAdapter() { + val tokenValidator: TokenValidator + /** Get the default token validator. */ + get() { + val loaderList = listOf( + JwksTokenVerifierLoader( + managementPortalProperties.authServer.serverAdminUrl + "/admin/keys/hydra.jwt.access-token", + ManagementPortalJwtAccessTokenConverter.RES_MANAGEMENT_PORTAL, + JwkAlgorithmParser() + ), + ) + return TokenValidator(loaderList).apply { + refresh() + } + } fun jwtAuthenticationFilter(): JwtAuthenticationFilter { return JwtAuthenticationFilter( - keyStoreHandler.tokenValidator, authenticationManager, userRepository + tokenValidator, authenticationManager, userRepository ) .skipUrlPattern(HttpMethod.GET, "/management/health") .skipUrlPattern(HttpMethod.POST, "/oauth/token") @@ -192,7 +224,7 @@ class OAuth2ServerConfiguration( @Throws(Exception::class) override fun configure(resources: ResourceServerSecurityConfigurer) { resources.resourceId("res_ManagementPortal") - .tokenStore(tokenStore) + //.tokenStore(tokenStore) .eventPublisher(CustomEventPublisher()) } @@ -212,64 +244,79 @@ class OAuth2ServerConfiguration( @Autowired @Qualifier("authenticationManagerBean") private val authenticationManager: AuthenticationManager, @Autowired private val dataSource: DataSource, @Autowired private val jdbcClientDetailsService: JdbcClientDetailsService, - @Autowired private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler + @Autowired private val managementPortalProperties: ManagementPortalProperties, + //@Autowired private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler ) : AuthorizationServerConfigurerAdapter() { - @Bean - protected fun authorizationCodeServices(): AuthorizationCodeServices { - return JdbcAuthorizationCodeServices(dataSource) + val tokenValidator: TokenValidator + get() { + val loaderList = listOf( + JwksTokenVerifierLoader( + managementPortalProperties.authServer.serverAdminUrl + "/admin/keys/hydra.jwt.access-token", + ManagementPortalJwtAccessTokenConverter.RES_MANAGEMENT_PORTAL, + JwkAlgorithmParser() + ), + ) + return TokenValidator(loaderList).apply { + refresh() + } } - @Bean - fun approvalStore(): ApprovalStore { - return if (jpaProperties.database == Database.POSTGRESQL) { - PostgresApprovalStore(dataSource) - } else { - // to have compatibility for other databases including H2 - JdbcApprovalStore(dataSource) - } - } +// @Bean +// protected fun authorizationCodeServices(): AuthorizationCodeServices { +// return JdbcAuthorizationCodeServices(dataSource) +// } - @Bean - fun tokenEnhancer(): TokenEnhancer { - return ClaimsTokenEnhancer() - } +// @Bean +// fun approvalStore(): ApprovalStore { +// return if (jpaProperties.database == Database.POSTGRESQL) { +// PostgresApprovalStore(dataSource) +// } else { +// // to have compatibility for other databases including H2 +// JdbcApprovalStore(dataSource) +// } +// } - @Bean - fun tokenStore(): TokenStore { - return ManagementPortalJwtTokenStore(accessTokenConverter()) - } +// @Bean +// fun tokenEnhancer(): TokenEnhancer { +// return ClaimsTokenEnhancer() +// } + +// @Bean +// fun tokenStore(): TokenStore { +// return ManagementPortalJwtTokenStore(accessTokenConverter()) +// } @Bean fun accessTokenConverter(): ManagementPortalJwtAccessTokenConverter { logger.debug("loading token converter from keystore configurations") return ManagementPortalJwtAccessTokenConverter( - keyStoreHandler.tokenValidator, - keyStoreHandler.algorithmForSigning, - keyStoreHandler.verifiers, - keyStoreHandler.refreshTokenVerifiers + tokenValidator, +// JWT.require(JwtTokenVerifier.DEFAULT_ALGORITHM) +// .build(), +// keyStoreHandler.refreshTokenVerifiers ) } - @Bean - @Primary - fun tokenServices(tokenStore: TokenStore?): DefaultTokenServices { - val defaultTokenServices = DefaultTokenServices() - defaultTokenServices.setTokenStore(tokenStore) - defaultTokenServices.setSupportRefreshToken(true) - defaultTokenServices.setReuseRefreshToken(false) - return defaultTokenServices - } +// @Bean +// @Primary +// fun tokenServices(tokenStore: TokenStore?): DefaultTokenServices { +// val defaultTokenServices = DefaultTokenServices() +// defaultTokenServices.setTokenStore(tokenStore) +// defaultTokenServices.setSupportRefreshToken(true) +// defaultTokenServices.setReuseRefreshToken(false) +// return defaultTokenServices +// } override fun configure(endpoints: AuthorizationServerEndpointsConfigurer) { val tokenEnhancerChain = TokenEnhancerChain() tokenEnhancerChain.setTokenEnhancers( - listOf(tokenEnhancer(), accessTokenConverter()) + listOf(accessTokenConverter())//tokenEnhancer(), accessTokenConverter()) ) endpoints - .authorizationCodeServices(authorizationCodeServices()) - .approvalStore(approvalStore()) - .tokenStore(tokenStore()) + //.authorizationCodeServices(authorizationCodeServices()) + //.approvalStore(approvalStore()) + //.tokenStore(tokenStore()) .tokenEnhancer(tokenEnhancerChain) .reuseRefreshTokens(false) .authenticationManager(authenticationManager) diff --git a/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt index 2205c18ac..9679cd9cc 100644 --- a/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt +++ b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt @@ -64,6 +64,7 @@ class JwtAuthenticationFilter @JvmOverloads constructor( httpResponse: HttpServletResponse, chain: FilterChain, ) { + Companion.logger.warn("Request: {}", httpServletRequestToString(httpRequest)) if (CorsUtils.isPreFlightRequest(httpRequest)) { Companion.logger.debug("Skipping JWT check for preflight request") chain.doFilter(httpRequest, httpResponse) @@ -79,6 +80,7 @@ class JwtAuthenticationFilter @JvmOverloads constructor( var exMessage = "No token provided" if (stringToken != null) { try { + Companion.logger.warn("Validating token from header: {}", stringToken) token = validator.validateBlocking(stringToken) Companion.logger.debug("Using token from header") } catch (ex: TokenValidationException) { @@ -100,6 +102,33 @@ class JwtAuthenticationFilter @JvmOverloads constructor( chain.doFilter(httpRequest, httpResponse) } + private fun httpServletRequestToString(httpRequest: HttpServletRequest):String { + val buffer = StringBuffer() + buffer.append("Method: ${httpRequest.method}\n") + buffer.append("RequestURI: ${httpRequest.requestURI}\n") + buffer.append("QueryString: ${httpRequest.queryString}\n") + buffer.append("RequestURL: ${httpRequest.requestURL}\n") + buffer.append("Protocol: ${httpRequest.protocol}\n") + buffer.append("Scheme: ${httpRequest.scheme}\n") + buffer.append("ServerName: ${httpRequest.serverName}\n") + buffer.append("ServerPort: ${httpRequest.serverPort}\n") + buffer.append("RemoteAddr: ${httpRequest.remoteAddr}\n") + buffer.append("RemoteHost: ${httpRequest.remoteHost}\n") + buffer.append("RemotePort: ${httpRequest.remotePort}\n") + buffer.append("LocalAddr: ${httpRequest.localAddr}\n") + buffer.append("LocalName: ${httpRequest.localName}\n") + buffer.append("LocalPort: ${httpRequest.localPort}\n") + buffer.append("AuthType: ${httpRequest.authType}\n") + buffer.append("ContentType: ${httpRequest.contentType}\n") + buffer.append("ContentLength: ${httpRequest.contentLength}\n") + buffer.append("CharacterEncoding: ${httpRequest.characterEncoding}\n") + buffer.append("Cookies: ${httpRequest.cookies}\n") + buffer.append("Headers: ${httpRequest.headerNames.toList().map { it to httpRequest.getHeaders(it).toList() }}\n") + buffer.append("Attributes: ${httpRequest.attributeNames.toList().map { it to httpRequest.getAttribute(it) }}\n") + buffer.append("Parameters: ${httpRequest.parameterMap.toList().map { it.first to it.second.toList() }}\n") + return buffer.toString() + } + override fun shouldNotFilter(@Nonnull httpRequest: HttpServletRequest): Boolean { val shouldNotFilterUrl = ignoreUrls.find { it.matches(httpRequest) } return if (shouldNotFilterUrl != null) { @@ -111,6 +140,7 @@ class JwtAuthenticationFilter @JvmOverloads constructor( } private fun tokenFromHeader(httpRequest: HttpServletRequest): String? { + Companion.logger.warn("Token from header: {}", httpRequest.getHeader(HttpHeaders.AUTHORIZATION)) return httpRequest.getHeader(HttpHeaders.AUTHORIZATION) ?.takeIf { it.startsWith(AUTHORIZATION_BEARER_HEADER) } ?.removePrefix(AUTHORIZATION_BEARER_HEADER) @@ -185,6 +215,7 @@ class JwtAuthenticationFilter @JvmOverloads constructor( private val logger = LoggerFactory.getLogger(JwtAuthenticationFilter::class.java) private const val AUTHORIZATION_BEARER_HEADER = "Bearer" private const val TOKEN_ATTRIBUTE = "jwt" + private const val TOKEN_COOKIE_NAME = "ory_kratos_session" /** * Authority references for given user. The user should have its roles mapped diff --git a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt index 88962f0a2..7a997ba36 100644 --- a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt +++ b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt @@ -36,9 +36,9 @@ import org.radarbase.auth.exception.TokenValidationException */ open class ManagementPortalJwtAccessTokenConverter( validator: TokenValidator, - algorithm: Algorithm, - verifiers: MutableList, - private val refreshTokenVerifiers: List +// algorithm: Algorithm, +// verifiers: MutableList, +// private val refreshTokenVerifiers: List ) : JwtAccessTokenConverter { private val jsonParser = ObjectMapper().readerFor( MutableMap::class.java @@ -63,7 +63,7 @@ open class ManagementPortalJwtAccessTokenConverter( } private var algorithm: Algorithm? = null private var validator: TokenValidator - private val verifiers: MutableList + //private val verifiers: MutableList /** * Default constructor. @@ -75,9 +75,9 @@ open class ManagementPortalJwtAccessTokenConverter( val accessToken = DefaultAccessTokenConverter() accessToken.setIncludeGrantType(true) tokenConverter = accessToken - this.verifiers = verifiers + //this.verifiers = verifiers this.validator = validator - setAlgorithm(algorithm) + //setAlgorithm(algorithm) } override fun convertAccessToken( @@ -102,9 +102,9 @@ open class ManagementPortalJwtAccessTokenConverter( override fun setAlgorithm(algorithm: Algorithm) { this.algorithm = algorithm - if (verifiers.isEmpty()) { - verifiers.add(JWT.require(algorithm).withAudience(RES_MANAGEMENT_PORTAL).build()) - } +// if (verifiers.isEmpty()) { +// verifiers.add(JWT.require(algorithm).withAudience(RES_MANAGEMENT_PORTAL).build()) +// } } /** @@ -212,7 +212,7 @@ open class ManagementPortalJwtAccessTokenConverter( override fun decode(token: String): Map { val jwt = JWT.decode(token) - val verifierToUse: List +// val verifierToUse: List val claims: MutableMap try { val decodedPayload = String( @@ -227,18 +227,18 @@ open class ManagementPortalJwtAccessTokenConverter( if (jwtClaimsSetVerifier != null) { jwtClaimsSetVerifier!!.verify(claims) } - verifierToUse = - if (claims[JwtAccessTokenConverter.ACCESS_TOKEN_ID] != null) refreshTokenVerifiers else verifiers +// verifierToUse = +// if (claims[JwtAccessTokenConverter.ACCESS_TOKEN_ID] != null) refreshTokenVerifiers else verifiers } catch (ex: JsonProcessingException) { throw InvalidTokenException("Invalid token", ex) } try { validator.validateBlocking(token) - Companion.logger.debug("Using token from header") + logger.debug("Using token from header") return claims } catch (ex: TokenValidationException) { ex.message?.let { - Companion.logger.info("Failed to validate token from header: {}", it) + logger.info("Failed to validate token from header: {}", it) } } throw InvalidTokenException("No registered validator could authenticate this token") diff --git a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt index 493cd9af3..eff469292 100644 --- a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt +++ b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt @@ -41,7 +41,7 @@ import kotlin.collections.Map.Entry * [org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory]. However, * this class does not assume a specific key type, while the Spring factory assumes RSA keys. */ -@Component +//@Component class ManagementPortalOauthKeyStoreHandler @Autowired constructor( environment: Environment, servletContext: ServletContext, private val managementPortalProperties: ManagementPortalProperties ) { @@ -73,7 +73,7 @@ class ManagementPortalOauthKeyStoreHandler @Autowired constructor( logger.info("Using Management Portal base-url {}", managementPortalBaseUrl) val algorithms = loadAlgorithmsFromAlias().filter { obj: Algorithm? -> Objects.nonNull(obj) }.toList() verifiers = algorithms.map { algo: Algorithm? -> - JWT.require(algo).withAudience(ManagementPortalJwtAccessTokenConverter.RES_MANAGEMENT_PORTAL).build() + JWT.require(algo).withAudience(RES_MANAGEMENT_PORTAL).build() }.toMutableList() // No need to check audience with a refresh token: it can be used // to refresh tokens intended for other resources. diff --git a/src/main/java/org/radarbase/management/service/MetaTokenService.kt b/src/main/java/org/radarbase/management/service/MetaTokenService.kt index 529011bd7..dcbdb0e3d 100644 --- a/src/main/java/org/radarbase/management/service/MetaTokenService.kt +++ b/src/main/java/org/radarbase/management/service/MetaTokenService.kt @@ -37,8 +37,8 @@ import javax.validation.ConstraintViolationException * Service to delegate MetaToken handling. * */ -@Service -@Transactional +//@Service +//@Transactional class MetaTokenService { @Autowired private val metaTokenRepository: MetaTokenRepository? = null diff --git a/src/main/java/org/radarbase/management/service/OAuthClientService.kt b/src/main/java/org/radarbase/management/service/OAuthClientService.kt index 7f09ce8f7..735d6a23f 100644 --- a/src/main/java/org/radarbase/management/service/OAuthClientService.kt +++ b/src/main/java/org/radarbase/management/service/OAuthClientService.kt @@ -28,7 +28,7 @@ import java.util.* * The service layer to handle OAuthClient and Token related functions. * Created by nivethika on 03/08/2018. */ -@Service +//@Service class OAuthClientService( @Autowired private val clientDetailsService: JdbcClientDetailsService, @Autowired private val clientDetailsMapper: ClientDetailsMapper, diff --git a/src/main/java/org/radarbase/management/service/mapper/decorator/ProjectMapperDecorator.kt b/src/main/java/org/radarbase/management/service/mapper/decorator/ProjectMapperDecorator.kt index 3fae78093..c51341b59 100644 --- a/src/main/java/org/radarbase/management/service/mapper/decorator/ProjectMapperDecorator.kt +++ b/src/main/java/org/radarbase/management/service/mapper/decorator/ProjectMapperDecorator.kt @@ -23,16 +23,16 @@ abstract class ProjectMapperDecorator : ProjectMapper { @Autowired @Qualifier("delegate") private lateinit var delegate: ProjectMapper @Autowired private lateinit var organizationRepository: OrganizationRepository @Autowired private lateinit var projectRepository: ProjectRepository - @Autowired private lateinit var metaTokenService: MetaTokenService + //@Autowired private lateinit var metaTokenService: MetaTokenService override fun projectToProjectDTO(project: Project?): ProjectDTO? { val dto = delegate.projectToProjectDTO(project) dto?.humanReadableProjectName = project?.attributes?.get(ProjectDTO.HUMAN_READABLE_PROJECT_NAME) - try { - dto?.persistentTokenTimeout = metaTokenService.getMetaTokenTimeout(true, project).toMillis() - } catch (ex: BadRequestException) { - dto?.persistentTokenTimeout = null - } +// try { +// dto?.persistentTokenTimeout = metaTokenService.getMetaTokenTimeout(true, project).toMillis() +// } catch (ex: BadRequestException) { +// dto?.persistentTokenTimeout = null +// } return dto } diff --git a/src/main/java/org/radarbase/management/web/rest/MetaTokenResource.kt b/src/main/java/org/radarbase/management/web/rest/MetaTokenResource.kt index bbf0a0b2b..7c9a412a5 100644 --- a/src/main/java/org/radarbase/management/web/rest/MetaTokenResource.kt +++ b/src/main/java/org/radarbase/management/web/rest/MetaTokenResource.kt @@ -20,8 +20,8 @@ import org.springframework.web.bind.annotation.RestController import java.net.MalformedURLException import java.time.Duration -@RestController -@RequestMapping("/api") +//@RestController +//@RequestMapping("/api") class MetaTokenResource { @Autowired private val metaTokenService: MetaTokenService? = null diff --git a/src/main/java/org/radarbase/management/web/rest/OAuthClientsResource.kt b/src/main/java/org/radarbase/management/web/rest/OAuthClientsResource.kt index ef7417585..68e9978d7 100644 --- a/src/main/java/org/radarbase/management/web/rest/OAuthClientsResource.kt +++ b/src/main/java/org/radarbase/management/web/rest/OAuthClientsResource.kt @@ -41,8 +41,8 @@ import javax.validation.Valid /** * Created by dverbeec on 5/09/2017. */ -@RestController -@RequestMapping("/api") +//@RestController +//@RequestMapping("/api") class OAuthClientsResource( @Autowired private val oAuthClientService: OAuthClientService, @Autowired private val metaTokenService: MetaTokenService, diff --git a/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt index c4fc357ee..e85db0c1b 100644 --- a/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt +++ b/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt @@ -8,7 +8,7 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController -@RestController +//@RestController class TokenKeyEndpoint @Autowired constructor( private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler ) { From 03c87ed7180cae4e807dee996c91407625d584a9 Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 20 Aug 2024 14:04:01 +0100 Subject: [PATCH 12/22] Fix ory stack configs --- src/main/docker/ory_stack.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/docker/ory_stack.yml b/src/main/docker/ory_stack.yml index c2f545d13..eb94e4391 100644 --- a/src/main/docker/ory_stack.yml +++ b/src/main/docker/ory_stack.yml @@ -5,12 +5,12 @@ volumes: services: radar-self-enrolment-ui: - image: ghcr.io/radar-base/radar-self-enrolment-ui:dev + image: ghcr.io/radar-base/radar-self-enrolment-ui:feat-consent-module environment: - ORY_SDK_URL=http://kratos:4433/ - HYDRA_ADMIN_URL=http://hydra:4445 ports: - - "3000:3000" + - "3002:4455" volumes: - /tmp/ui-node/logs:/root/.npm/_logs @@ -26,14 +26,14 @@ services: - DSN=postgres://ory:secret@postgresd-ory/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_HOOK=web_hook - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_METHOD=POST - - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_URL=http://managementportal-app:8080/managementportal/api/kratos/subjects - - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_BODY=file:///etc/config/kratos/webhook_body.jsonnet + - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_URL=http://managementportal:8080/managementportal/api/kratos/subjects + - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_BODY=/etc/config/kratos/webhook_body.jsonnet - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_RESPONSE_IGNORE=true - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_1_HOOK=session - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_HOOK=web_hook - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_METHOD=POST - - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_URL=http://managementportal-app:8080/managementportal/api/kratos/subjects/activate - - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_BODY=file:///etc/config/kratos/webhook_body.jsonnet + - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_URL=http://managementportal:8080/managementportal/api/kratos/subjects/activate + - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_BODY=/etc/config/kratos/webhook_body.jsonnet command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier volumes: - type: bind From 1882851fc1e6b07594664de3615873263e1249ec Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 22 Aug 2024 15:39:51 +0100 Subject: [PATCH 13/22] Remove OAuth2ServerConfiguration and use single SecurityConfig for auth --- .../config/OAuth2ServerConfiguration.kt | 341 ------------------ .../config/SecurityConfiguration.kt | 181 ++++++---- .../ManagementPortalOauthKeyStoreHandler.kt | 302 ---------------- .../management/web/rest/TokenKeyEndpoint.kt | 6 +- src/main/resources/config/application-dev.yml | 2 +- .../resources/config/application-prod.yml | 2 +- 6 files changed, 114 insertions(+), 720 deletions(-) delete mode 100644 src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt delete mode 100644 src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt diff --git a/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt b/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt deleted file mode 100644 index bce2afc2a..000000000 --- a/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt +++ /dev/null @@ -1,341 +0,0 @@ -package org.radarbase.management.config - -import com.auth0.jwt.JWT -import org.radarbase.auth.authentication.TokenValidator -import java.util.* -import javax.sql.DataSource -import org.radarbase.auth.authorization.RoleAuthority -import org.radarbase.auth.jwks.JwkAlgorithmParser -import org.radarbase.auth.jwks.JwksTokenVerifierLoader -import org.radarbase.auth.jwt.JwtTokenVerifier -import org.radarbase.management.repository.UserRepository -import org.radarbase.management.security.ClaimsTokenEnhancer -import org.radarbase.management.security.Http401UnauthorizedEntryPoint -import org.radarbase.management.security.JwtAuthenticationFilter -import org.radarbase.management.security.PostgresApprovalStore -import org.radarbase.management.security.jwt.ManagementPortalJwtAccessTokenConverter -import org.radarbase.management.security.jwt.ManagementPortalJwtTokenStore -import org.radarbase.management.security.jwt.ManagementPortalOauthKeyStoreHandler -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Primary -import org.springframework.core.annotation.Order -import org.springframework.http.HttpMethod -import org.springframework.orm.jpa.vendor.Database -import org.springframework.security.authentication.AuthenticationManager -import org.springframework.security.authentication.DefaultAuthenticationEventPublisher -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder -import org.springframework.security.config.annotation.web.builders.HttpSecurity -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter -import org.springframework.security.config.http.SessionCreationPolicy -import org.springframework.security.core.Authentication -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder -import org.springframework.security.crypto.password.PasswordEncoder -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer -import org.springframework.security.oauth2.provider.approval.ApprovalStore -import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore -import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService -import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices -import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices -import org.springframework.security.oauth2.provider.token.DefaultTokenServices -import org.springframework.security.oauth2.provider.token.TokenEnhancer -import org.springframework.security.oauth2.provider.token.TokenEnhancerChain -import org.springframework.security.oauth2.provider.token.TokenStore -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter -import org.springframework.security.web.authentication.logout.LogoutSuccessHandler - -@Configuration -class OAuth2ServerConfiguration( - @Autowired private val dataSource: DataSource, - @Autowired private val passwordEncoder: PasswordEncoder, -) { - @Configuration - @Order(-20) - protected class LoginConfig( - @Autowired private val authenticationManager: AuthenticationManager, - @Autowired private val jwtAuthenticationFilter: JwtAuthenticationFilter - ) : WebSecurityConfigurerAdapter() { - - @Throws(Exception::class) - override fun configure(http: HttpSecurity) { - http - .formLogin().loginPage("/login").permitAll() - .and() - .authorizeRequests() - .antMatchers("/oauth/token").permitAll() - .and() - .addFilterAfter( - jwtAuthenticationFilter, - UsernamePasswordAuthenticationFilter::class.java - ) - .requestMatchers() - .antMatchers("/login", "/oauth/authorize", "/oauth/confirm_access") - .and() - .authorizeRequests().anyRequest().authenticated() - } - - @Throws(Exception::class) - override fun configure(auth: AuthenticationManagerBuilder) { - auth.parentAuthenticationManager(authenticationManager) - } - } - - @Configuration - class JwtAuthenticationFilterConfiguration( - @Autowired private val authenticationManager: AuthenticationManager, - @Autowired private val userRepository: UserRepository, - @Autowired private val managementPortalProperties: ManagementPortalProperties, - //@Autowired private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler - ) { - val tokenValidator: TokenValidator - /** Get the default token validator. */ - get() { - val loaderList = listOf( - JwksTokenVerifierLoader( - managementPortalProperties.authServer.serverAdminUrl + "/admin/keys/hydra.jwt.access-token", - ManagementPortalJwtAccessTokenConverter.RES_MANAGEMENT_PORTAL, - JwkAlgorithmParser() - ), - ) - return TokenValidator(loaderList) - } - @Bean - fun jwtAuthenticationFilter(): JwtAuthenticationFilter { - return JwtAuthenticationFilter( - tokenValidator, - authenticationManager, - userRepository, - true - ) - } - } - - @Bean - fun jdbcClientDetailsService(): JdbcClientDetailsService { - val clientDetailsService = JdbcClientDetailsService(dataSource) - clientDetailsService.setPasswordEncoder(passwordEncoder) - return clientDetailsService - } - - @Configuration - @EnableResourceServer - protected class ResourceServerConfiguration( - //@Autowired private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler, - //@Autowired private val tokenStore: TokenStore, - @Autowired private val managementPortalProperties: ManagementPortalProperties, - @Autowired private val http401UnauthorizedEntryPoint: Http401UnauthorizedEntryPoint, - @Autowired private val logoutSuccessHandler: LogoutSuccessHandler, - @Autowired private val authenticationManager: AuthenticationManager, - @Autowired private val userRepository: UserRepository - ) : ResourceServerConfigurerAdapter() { - val tokenValidator: TokenValidator - /** Get the default token validator. */ - get() { - val loaderList = listOf( - JwksTokenVerifierLoader( - managementPortalProperties.authServer.serverAdminUrl + "/admin/keys/hydra.jwt.access-token", - ManagementPortalJwtAccessTokenConverter.RES_MANAGEMENT_PORTAL, - JwkAlgorithmParser() - ), - ) - return TokenValidator(loaderList).apply { - refresh() - } - } - - fun jwtAuthenticationFilter(): JwtAuthenticationFilter { - return JwtAuthenticationFilter( - tokenValidator, authenticationManager, userRepository - ) - .skipUrlPattern(HttpMethod.GET, "/management/health") - .skipUrlPattern(HttpMethod.POST, "/oauth/token") - .skipUrlPattern(HttpMethod.GET, "/api/meta-token/*") - .skipUrlPattern(HttpMethod.GET, "/api/public/projects") - .skipUrlPattern(HttpMethod.GET, "/api/sitesettings") - .skipUrlPattern(HttpMethod.GET, "/api/redirect/**") - .skipUrlPattern(HttpMethod.GET, "/api/logout-url") - .skipUrlPattern(HttpMethod.GET, "/oauth2/authorize") - .skipUrlPattern(HttpMethod.GET, "/images/**") - .skipUrlPattern(HttpMethod.GET, "/css/**") - .skipUrlPattern(HttpMethod.GET, "/js/**") - .skipUrlPattern(HttpMethod.GET, "/radar-baseRR.png") - } - - @Throws(Exception::class) - override fun configure(http: HttpSecurity) { - http - .exceptionHandling() - .authenticationEntryPoint(http401UnauthorizedEntryPoint) - .and() - .addFilterBefore( - jwtAuthenticationFilter(), - UsernamePasswordAuthenticationFilter::class.java - ) - .authorizeRequests() - .antMatchers("/oauth/**").permitAll() - .and() - .logout().invalidateHttpSession(true) - .logoutUrl("/api/logout") - .logoutSuccessHandler(logoutSuccessHandler) - .and() - .headers() - .frameOptions() - .disable() - .and() - .sessionManagement() - .sessionCreationPolicy(SessionCreationPolicy.ALWAYS) - .and() - .addFilterBefore( - jwtAuthenticationFilter(), - UsernamePasswordAuthenticationFilter::class.java - ) - .authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() - .antMatchers("/api/register") - .hasAnyAuthority(RoleAuthority.SYS_ADMIN_AUTHORITY) - .antMatchers("/api/profile-info").permitAll() - .antMatchers("/api/sitesettings").permitAll() - .antMatchers("/api/public/projects").permitAll() - .antMatchers("/api/logout-url").permitAll() - .antMatchers("/api/**") - .authenticated() // Allow management/health endpoint to all to allow kubernetes to be able to - // detect the health of the service - .antMatchers("/oauth/token").permitAll() - .antMatchers("/management/health").permitAll() - .antMatchers("/management/**") - .hasAnyAuthority(RoleAuthority.SYS_ADMIN_AUTHORITY) - .antMatchers("/v2/api-docs/**").permitAll() - .antMatchers("/swagger-resources/configuration/ui").permitAll() - .antMatchers("/swagger-ui/index.html") - .hasAnyAuthority(RoleAuthority.SYS_ADMIN_AUTHORITY) - } - - @Throws(Exception::class) - override fun configure(resources: ResourceServerSecurityConfigurer) { - resources.resourceId("res_ManagementPortal") - //.tokenStore(tokenStore) - .eventPublisher(CustomEventPublisher()) - } - - protected class CustomEventPublisher : DefaultAuthenticationEventPublisher() { - override fun publishAuthenticationSuccess(authentication: Authentication) { - // OAuth2AuthenticationProcessingFilter publishes an authentication success audit - // event for EVERY successful OAuth request to our API resources, this is way too - // much so we override the event publisher to not publish these events. - } - } - } - - @Configuration - @EnableAuthorizationServer - protected class AuthorizationServerConfiguration( - @Autowired private val jpaProperties: JpaProperties, - @Autowired @Qualifier("authenticationManagerBean") private val authenticationManager: AuthenticationManager, - @Autowired private val dataSource: DataSource, - @Autowired private val jdbcClientDetailsService: JdbcClientDetailsService, - @Autowired private val managementPortalProperties: ManagementPortalProperties, - //@Autowired private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler - ) : AuthorizationServerConfigurerAdapter() { - - val tokenValidator: TokenValidator - get() { - val loaderList = listOf( - JwksTokenVerifierLoader( - managementPortalProperties.authServer.serverAdminUrl + "/admin/keys/hydra.jwt.access-token", - ManagementPortalJwtAccessTokenConverter.RES_MANAGEMENT_PORTAL, - JwkAlgorithmParser() - ), - ) - return TokenValidator(loaderList).apply { - refresh() - } - } - -// @Bean -// protected fun authorizationCodeServices(): AuthorizationCodeServices { -// return JdbcAuthorizationCodeServices(dataSource) -// } - -// @Bean -// fun approvalStore(): ApprovalStore { -// return if (jpaProperties.database == Database.POSTGRESQL) { -// PostgresApprovalStore(dataSource) -// } else { -// // to have compatibility for other databases including H2 -// JdbcApprovalStore(dataSource) -// } -// } - -// @Bean -// fun tokenEnhancer(): TokenEnhancer { -// return ClaimsTokenEnhancer() -// } - -// @Bean -// fun tokenStore(): TokenStore { -// return ManagementPortalJwtTokenStore(accessTokenConverter()) -// } - - @Bean - fun accessTokenConverter(): ManagementPortalJwtAccessTokenConverter { - logger.debug("loading token converter from keystore configurations") - return ManagementPortalJwtAccessTokenConverter( - tokenValidator, -// JWT.require(JwtTokenVerifier.DEFAULT_ALGORITHM) -// .build(), -// keyStoreHandler.refreshTokenVerifiers - ) - } - -// @Bean -// @Primary -// fun tokenServices(tokenStore: TokenStore?): DefaultTokenServices { -// val defaultTokenServices = DefaultTokenServices() -// defaultTokenServices.setTokenStore(tokenStore) -// defaultTokenServices.setSupportRefreshToken(true) -// defaultTokenServices.setReuseRefreshToken(false) -// return defaultTokenServices -// } - - override fun configure(endpoints: AuthorizationServerEndpointsConfigurer) { - val tokenEnhancerChain = TokenEnhancerChain() - tokenEnhancerChain.setTokenEnhancers( - listOf(accessTokenConverter())//tokenEnhancer(), accessTokenConverter()) - ) - endpoints - //.authorizationCodeServices(authorizationCodeServices()) - //.approvalStore(approvalStore()) - //.tokenStore(tokenStore()) - .tokenEnhancer(tokenEnhancerChain) - .reuseRefreshTokens(false) - .authenticationManager(authenticationManager) - } - - override fun configure(oauthServer: AuthorizationServerSecurityConfigurer) { - oauthServer.allowFormAuthenticationForClients() - .checkTokenAccess("isAuthenticated()") - .tokenKeyAccess("permitAll()") - .passwordEncoder(BCryptPasswordEncoder()) - } - - @Throws(Exception::class) - override fun configure(clients: ClientDetailsServiceConfigurer) { - clients.withClientDetails(jdbcClientDetailsService) - } - } - - companion object { - private val logger = LoggerFactory.getLogger(OAuth2ServerConfiguration::class.java) - } -} diff --git a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt index 311c1badb..3a05aae8f 100644 --- a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt +++ b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt @@ -1,6 +1,11 @@ package org.radarbase.management.config +import org.radarbase.auth.authentication.TokenValidator +import org.radarbase.auth.jwks.JwkAlgorithmParser +import org.radarbase.auth.jwks.JwksTokenVerifierLoader +import org.radarbase.management.repository.UserRepository import org.radarbase.management.security.Http401UnauthorizedEntryPoint +import org.radarbase.management.security.JwtAuthenticationFilter // Make sure to import this import org.radarbase.management.security.RadarAuthenticationProvider import org.springframework.beans.factory.BeanInitializationException import org.springframework.beans.factory.annotation.Autowired @@ -17,9 +22,8 @@ import org.springframework.security.config.annotation.web.builders.WebSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter import org.springframework.security.config.http.SessionCreationPolicy -import org.springframework.security.core.userdetails.UserDetailsService -import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.data.repository.query.SecurityEvaluationContextExtension +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.springframework.security.web.authentication.logout.LogoutSuccessHandler import tech.jhipster.security.AjaxLogoutSuccessHandler import javax.annotation.PostConstruct @@ -28,80 +32,115 @@ import javax.annotation.PostConstruct @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) class SecurityConfiguration -/** Security configuration constructor. */ @Autowired constructor( - private val authenticationManagerBuilder: AuthenticationManagerBuilder, - private val userDetailsService: UserDetailsService, - private val applicationEventPublisher: ApplicationEventPublisher, - private val passwordEncoder: PasswordEncoder -) : WebSecurityConfigurerAdapter() { - @PostConstruct - fun init() { - try { - authenticationManagerBuilder - .userDetailsService(userDetailsService) - .passwordEncoder(passwordEncoder) - .and() - .authenticationProvider(RadarAuthenticationProvider()) - .authenticationEventPublisher( - DefaultAuthenticationEventPublisher(applicationEventPublisher) - ) - } catch (e: Exception) { - throw BeanInitializationException("Security configuration failed", e) +/** Security configuration constructor. */ + @Autowired + constructor( + private val authenticationManagerBuilder: AuthenticationManagerBuilder, + private val applicationEventPublisher: ApplicationEventPublisher, + private val userRepository: UserRepository, + @Autowired private val managementPortalProperties: ManagementPortalProperties, + ) : WebSecurityConfigurerAdapter() { + val tokenValidator: TokenValidator + /** Get the default token validator. */ + get() { + val loaderList = + listOf( + JwksTokenVerifierLoader( + managementPortalProperties.authServer.serverAdminUrl + + "/admin/keys/hydra.jwt.access-token", + RES_MANAGEMENT_PORTAL, + JwkAlgorithmParser(), + ), + ) + return TokenValidator(loaderList) + } + + @PostConstruct + fun init() { + try { + authenticationManagerBuilder + .authenticationProvider(RadarAuthenticationProvider()) + .authenticationEventPublisher( + DefaultAuthenticationEventPublisher(applicationEventPublisher), + ) + } catch (e: Exception) { + throw BeanInitializationException("Security configuration failed", e) + } } - } - @Bean - fun logoutSuccessHandler(): LogoutSuccessHandler { - return AjaxLogoutSuccessHandler() - } + @Bean fun logoutSuccessHandler(): LogoutSuccessHandler = AjaxLogoutSuccessHandler() - @Bean - fun http401UnauthorizedEntryPoint(): Http401UnauthorizedEntryPoint { - return Http401UnauthorizedEntryPoint() - } + @Bean + fun http401UnauthorizedEntryPoint(): Http401UnauthorizedEntryPoint = Http401UnauthorizedEntryPoint() - override fun configure(web: WebSecurity) { - web.ignoring() - .antMatchers("/") - .antMatchers("/*.{js,ico,css,html}") - .antMatchers(HttpMethod.OPTIONS, "/**") - .antMatchers("/app/**/*.{js,html}") - .antMatchers("/bower_components/**") - .antMatchers("/i18n/**") - .antMatchers("/content/**") - .antMatchers("/swagger-ui/**") - .antMatchers("/api-docs/**") - .antMatchers("/swagger-ui.html") - .antMatchers("/api-docs{,.json,.yml}") - .antMatchers("/api/register") - .antMatchers("/api/logout-url") - .antMatchers("/api/profile-info") - .antMatchers("/api/activate") - .antMatchers("/api/redirect/**") - .antMatchers("/api/account/reset_password/init") - .antMatchers("/api/account/reset_password/finish") - .antMatchers("/test/**") - .antMatchers("/management/health") - .antMatchers(HttpMethod.GET, "/api/meta-token/**") - } + @Bean + fun jwtAuthenticationFilter(): JwtAuthenticationFilter = + JwtAuthenticationFilter(tokenValidator, authenticationManager(), userRepository) + .skipUrlPattern(HttpMethod.GET, "/management/health") + .skipUrlPattern(HttpMethod.POST, "/oauth/token") + .skipUrlPattern(HttpMethod.GET, "/api/meta-token/*") + .skipUrlPattern(HttpMethod.GET, "/api/public/projects") + .skipUrlPattern(HttpMethod.GET, "/api/sitesettings") + .skipUrlPattern(HttpMethod.GET, "/api/redirect/**") + .skipUrlPattern(HttpMethod.GET, "/api/profile-info") + .skipUrlPattern(HttpMethod.GET, "/api/logout-url") + .skipUrlPattern(HttpMethod.GET, "/oauth2/authorize") + .skipUrlPattern(HttpMethod.GET, "/images/**") + .skipUrlPattern(HttpMethod.GET, "/css/**") + .skipUrlPattern(HttpMethod.GET, "/js/**") + .skipUrlPattern(HttpMethod.GET, "/radar-baseRR.png") - @Throws(Exception::class) - public override fun configure(http: HttpSecurity) { - http - .httpBasic().realmName("ManagementPortal") - .and() - .sessionManagement() - .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) - } + override fun configure(web: WebSecurity) { + web.ignoring() + .antMatchers("/") + .antMatchers("/*.{js,ico,css,html}") + .antMatchers(HttpMethod.OPTIONS, "/**") + .antMatchers("/app/**/*.{js,html}") + .antMatchers("/bower_components/**") + .antMatchers("/i18n/**") + .antMatchers("/content/**") + .antMatchers("/swagger-ui/**") + .antMatchers("/api-docs/**") + .antMatchers("/swagger-ui.html") + .antMatchers("/api-docs{,.json,.yml}") + .antMatchers("/api/login") + .antMatchers("/api/logout-url") + .antMatchers("/api/profile-info") + .antMatchers("/api/activate") + .antMatchers("/api/redirect/**") + .antMatchers("/api/account/reset_password/init") + .antMatchers("/api/account/reset_password/finish") + .antMatchers("/test/**") + .antMatchers("/management/health") + .antMatchers(HttpMethod.GET, "/api/meta-token/**") + } - @Bean - @Throws(Exception::class) - override fun authenticationManagerBean(): AuthenticationManager { - return super.authenticationManagerBean() - } + @Throws(Exception::class) + public override fun configure(http: HttpSecurity) { + http + .csrf().disable() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) + .and() + .exceptionHandling() + .authenticationEntryPoint(http401UnauthorizedEntryPoint()) + .and() + .addFilterBefore( + jwtAuthenticationFilter(), + UsernamePasswordAuthenticationFilter::class.java, + ) + .authorizeRequests() + .anyRequest().authenticated() + } + + @Bean + @Throws(Exception::class) + override fun authenticationManagerBean(): AuthenticationManager = super.authenticationManagerBean() - @Bean - fun securityEvaluationContextExtension(): SecurityEvaluationContextExtension { - return SecurityEvaluationContextExtension() + @Bean + fun securityEvaluationContextExtension(): SecurityEvaluationContextExtension = SecurityEvaluationContextExtension() + + companion object { + const val RES_MANAGEMENT_PORTAL = "res_ManagementPortal" + } } -} diff --git a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt deleted file mode 100644 index eff469292..000000000 --- a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt +++ /dev/null @@ -1,302 +0,0 @@ -package org.radarbase.management.security.jwt - -import com.auth0.jwt.JWT -import com.auth0.jwt.JWTVerifier -import com.auth0.jwt.algorithms.Algorithm -import org.radarbase.auth.authentication.TokenValidator -import org.radarbase.auth.jwks.JsonWebKeySet -import org.radarbase.auth.jwks.JwkAlgorithmParser -import org.radarbase.auth.jwks.JwksTokenVerifierLoader -import org.radarbase.management.config.ManagementPortalProperties -import org.radarbase.management.config.ManagementPortalProperties.Oauth -import org.radarbase.management.security.jwt.ManagementPortalJwtAccessTokenConverter.Companion.RES_MANAGEMENT_PORTAL -import org.radarbase.management.security.jwt.algorithm.EcdsaJwtAlgorithm -import org.radarbase.management.security.jwt.algorithm.JwtAlgorithm -import org.radarbase.management.security.jwt.algorithm.RsaJwtAlgorithm -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.core.env.Environment -import org.springframework.core.io.ClassPathResource -import org.springframework.core.io.Resource -import org.springframework.stereotype.Component -import java.io.IOException -import java.lang.IllegalArgumentException -import java.security.KeyPair -import java.security.KeyStore -import java.security.KeyStoreException -import java.security.NoSuchAlgorithmException -import java.security.PrivateKey -import java.security.UnrecoverableKeyException -import java.security.cert.CertificateException -import java.security.interfaces.ECPrivateKey -import java.security.interfaces.RSAPrivateKey -import java.util.* -import java.util.AbstractMap.SimpleImmutableEntry -import javax.annotation.Nonnull -import javax.servlet.ServletContext -import kotlin.collections.Map.Entry - -/** - * Similar to Spring's - * [org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory]. However, - * this class does not assume a specific key type, while the Spring factory assumes RSA keys. - */ -//@Component -class ManagementPortalOauthKeyStoreHandler @Autowired constructor( - environment: Environment, servletContext: ServletContext, private val managementPortalProperties: ManagementPortalProperties -) { - private val password: CharArray - private val store: KeyStore - private val loadedResource: Resource - private val oauthConfig: Oauth - private val verifierPublicKeyAliasList: List - private val managementPortalBaseUrl: String - val verifiers: MutableList - val refreshTokenVerifiers: MutableList - - /** - * Keystore factory. This tries to load the first valid keystore listed in resources. - * - * @throws IllegalArgumentException if none of the provided resources can be used to load a - * keystore. - */ - init { - checkOAuthConfig(managementPortalProperties) - oauthConfig = managementPortalProperties.oauth - password = oauthConfig.keyStorePassword.toCharArray() - val loadedStore: Entry = loadStore() - loadedResource = loadedStore.key - store = loadedStore.value - verifierPublicKeyAliasList = loadVerifiersPublicKeyAliasList() - managementPortalBaseUrl = - ("http://localhost:" + environment.getProperty("server.port") + servletContext.contextPath) - logger.info("Using Management Portal base-url {}", managementPortalBaseUrl) - val algorithms = loadAlgorithmsFromAlias().filter { obj: Algorithm? -> Objects.nonNull(obj) }.toList() - verifiers = algorithms.map { algo: Algorithm? -> - JWT.require(algo).withAudience(RES_MANAGEMENT_PORTAL).build() - }.toMutableList() - // No need to check audience with a refresh token: it can be used - // to refresh tokens intended for other resources. - refreshTokenVerifiers = algorithms.map { algo: Algorithm -> JWT.require(algo).build() }.toMutableList() - tokenValidator.refresh() - } - - @Nonnull - private fun loadStore(): Entry { - for (resource in KEYSTORE_PATHS) { - if (!resource.exists()) { - logger.debug("JWT key store {} does not exist. Ignoring this resource", resource) - continue - } - try { - val fileName = Objects.requireNonNull(resource.filename).lowercase() - val type = if (fileName.endsWith(".pfx") || fileName.endsWith(".p12")) "PKCS12" else "jks" - val localStore = KeyStore.getInstance(type) - localStore.load(resource.inputStream, password) - logger.debug("Loaded JWT key store {}", resource) - if (localStore != null) - return SimpleImmutableEntry(resource, localStore) - } catch (ex: CertificateException) { - logger.error("Cannot load JWT key store", ex) - } catch (ex: NoSuchAlgorithmException) { - logger.error("Cannot load JWT key store", ex) - } catch (ex: KeyStoreException) { - logger.error("Cannot load JWT key store", ex) - } catch (ex: IOException) { - logger.error("Cannot load JWT key store", ex) - } - } - throw IllegalArgumentException( - "Cannot load any of the given JWT key stores " + KEYSTORE_PATHS - ) - } - - private fun loadVerifiersPublicKeyAliasList(): List { - val publicKeyAliases: MutableList = ArrayList() - oauthConfig.signingKeyAlias?.let { publicKeyAliases.add(it) } - if (oauthConfig.checkingKeyAliases != null) { - publicKeyAliases.addAll(oauthConfig.checkingKeyAliases!!) - } - return publicKeyAliases - } - - /** - * Returns configured public keys of token verifiers. - * @return List of public keys for token verification. - */ - fun loadJwks(): JsonWebKeySet { - return JsonWebKeySet(verifierPublicKeyAliasList.map { alias: String -> this.getKeyPair(alias) } - .map { keyPair: KeyPair? -> getJwtAlgorithm(keyPair) }.mapNotNull { obj: JwtAlgorithm? -> obj?.jwk }) - } - - /** - * Load default verifiers from configured keystore and aliases. - */ - private fun loadAlgorithmsFromAlias(): Collection { - return verifierPublicKeyAliasList - .map { alias: String -> this.getKeyPair(alias) } - .mapNotNull { keyPair -> getJwtAlgorithm(keyPair) } - .map { obj: JwtAlgorithm -> obj.algorithm } - } - - val algorithmForSigning: Algorithm - /** - * Returns the signing algorithm extracted based on signing alias configured from keystore. - * @return signing algorithm. - */ - get() { - val signKey = oauthConfig.signingKeyAlias - logger.debug("Using JWT signing key {}", signKey) - val keyPair = getKeyPair(signKey) ?: throw IllegalArgumentException( - "Cannot load JWT signing key " + signKey + " from JWT key store." - ) - return getAlgorithmFromKeyPair(keyPair) - } - - /** - * Get a key pair from the store using the store password. - * @param alias key pair alias - * @return loaded key pair or `null` if the key store does not contain a loadable key with - * given alias. - * @throws IllegalArgumentException if the key alias password is wrong or the key cannot - * loaded. - */ - private fun getKeyPair(alias: String): KeyPair? { - return getKeyPair(alias, password) - } - - /** - * Get a key pair from the store with a given alias and password. - * @param alias key pair alias - * @param password key pair password - * @return loaded key pair or `null` if the key store does not contain a loadable key with - * given alias. - * @throws IllegalArgumentException if the key alias password is wrong or the key cannot - * load. - */ - private fun getKeyPair(alias: String, password: CharArray): KeyPair? { - return try { - val key = store.getKey(alias, password) as PrivateKey? - if (key == null) { - logger.warn( - "JWT key store {} does not contain private key pair for alias {}", loadedResource, alias - ) - return null - } - val cert = store.getCertificate(alias) - if (cert == null) { - logger.warn( - "JWT key store {} does not contain certificate pair for alias {}", loadedResource, alias - ) - return null - } - val publicKey = cert.publicKey - if (publicKey == null) { - logger.warn( - "JWT key store {} does not contain public key pair for alias {}", loadedResource, alias - ) - return null - } - KeyPair(publicKey, key) - } catch (ex: NoSuchAlgorithmException) { - logger.warn( - "JWT key store {} contains unknown algorithm for key pair with alias {}: {}", - loadedResource, - alias, - ex.toString() - ) - null - } catch (ex: UnrecoverableKeyException) { - throw IllegalArgumentException( - "JWT key store $loadedResource contains unrecoverable key pair with alias $alias (the password may be wrong)", - ex - ) - } catch (ex: KeyStoreException) { - throw IllegalArgumentException( - "JWT key store $loadedResource contains unrecoverable key pair with alias $alias (the password may be wrong)", - ex - ) - } - } - - val tokenValidator: TokenValidator - /** Get the default token validator. */ - get() { - val loaderList = listOf( - JwksTokenVerifierLoader( - managementPortalBaseUrl + "/oauth/token_key", - RES_MANAGEMENT_PORTAL, - JwkAlgorithmParser() - ), - managementPortalProperties.authServer.let { - JwksTokenVerifierLoader( - it.serverAdminUrl + "/admin/keys/hydra.jwt.access-token", - RES_MANAGEMENT_PORTAL, - JwkAlgorithmParser() - ) - }, - ) - return TokenValidator(loaderList) - } - - companion object { - private val logger = LoggerFactory.getLogger( - ManagementPortalOauthKeyStoreHandler::class.java - ) - private val KEYSTORE_PATHS = listOf( - ClassPathResource("/config/keystore.p12"), ClassPathResource("/config/keystore.jks") - ) - - private fun checkOAuthConfig(managementPortalProperties: ManagementPortalProperties) { - val oauthConfig = managementPortalProperties.oauth - if (oauthConfig.keyStorePassword.isEmpty()) { - logger.error("oauth.keyStorePassword is empty") - throw IllegalArgumentException("oauth.keyStorePassword is empty") - } - if (oauthConfig.signingKeyAlias == null || oauthConfig.signingKeyAlias!!.isEmpty()) { - logger.error("oauth.signingKeyAlias is empty") - throw IllegalArgumentException("OauthConfig is not provided") - } - } - - /** - * Returns extracted [Algorithm] from the KeyPair. - * @param keyPair to find algorithm. - * @return extracted algorithm. - */ - private fun getAlgorithmFromKeyPair(keyPair: KeyPair): Algorithm { - val alg = getJwtAlgorithm(keyPair) ?: throw IllegalArgumentException( - "KeyPair type " + keyPair.private.algorithm + " is unknown." - ) - return alg.algorithm - } - - /** - * Get the JWT algorithm to sign or verify JWTs with. - * @param keyPair key pair for signing/verifying. - * @return algorithm or `null` if the key type is unknown. - */ - private fun getJwtAlgorithm(keyPair: KeyPair?): JwtAlgorithm? { - if (keyPair == null) { - return null - } - val privateKey = keyPair.private - return when (privateKey) { - is ECPrivateKey -> { - EcdsaJwtAlgorithm(keyPair) - } - - is RSAPrivateKey -> { - RsaJwtAlgorithm(keyPair) - } - - else -> { - logger.warn( - "No JWT algorithm found for key type {}", privateKey.javaClass - ) - null - } - } - } - } -} diff --git a/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt index e85db0c1b..2e7e5c400 100644 --- a/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt +++ b/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt @@ -2,7 +2,6 @@ package org.radarbase.management.web.rest import io.micrometer.core.annotation.Timed import org.radarbase.auth.jwks.JsonWebKeySet -import org.radarbase.management.security.jwt.ManagementPortalOauthKeyStoreHandler import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.web.bind.annotation.GetMapping @@ -10,11 +9,10 @@ import org.springframework.web.bind.annotation.RestController //@RestController class TokenKeyEndpoint @Autowired constructor( - private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler ) { @get:Timed @get:GetMapping("/oauth/token_key") - val key: JsonWebKeySet + val key: JsonWebKeySet? /** * Get the verification key for the token signatures. The principal has to * be provided only if the key is secret @@ -23,7 +21,7 @@ class TokenKeyEndpoint @Autowired constructor( */ get() { logger.debug("Requesting verifier public keys...") - return keyStoreHandler.loadJwks() + return null } companion object { diff --git a/src/main/resources/config/application-dev.yml b/src/main/resources/config/application-dev.yml index 76a54b5b7..13a6d3fd2 100644 --- a/src/main/resources/config/application-dev.yml +++ b/src/main/resources/config/application-dev.yml @@ -101,7 +101,7 @@ managementportal: from: ManagementPortal@localhost frontend: clientId: ManagementPortalapp - clientSecret: my-secret-token-to-change-in-production + clientSecret: secret accessTokenValiditySeconds: 14400 refreshTokenValiditySeconds: 259200 sessionTimeout : 86400 # session for rft cookie diff --git a/src/main/resources/config/application-prod.yml b/src/main/resources/config/application-prod.yml index 436de6e62..01ae180aa 100644 --- a/src/main/resources/config/application-prod.yml +++ b/src/main/resources/config/application-prod.yml @@ -89,7 +89,7 @@ managementportal: from: ManagementPortal@localhost frontend: clientId: ManagementPortalapp - clientSecret: + clientSecret: secret accessTokenValiditySeconds: 14400 refreshTokenValiditySeconds: 259200 sessionTimeout: 86400 From 7600ae8f0fd341de76d87717358bf4e59a2d70be Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 22 Aug 2024 23:53:47 +0100 Subject: [PATCH 14/22] Refactor JwtAuthenticationFilter to accept jwt and session data --- .../config/SecurityConfiguration.kt | 2 +- .../security/JwtAuthenticationFilter.kt | 280 +++++++----------- .../auth/authentication/OAuthHelper.kt | 2 +- 3 files changed, 105 insertions(+), 179 deletions(-) diff --git a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt index 3a05aae8f..446f691b4 100644 --- a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt +++ b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt @@ -75,7 +75,7 @@ class SecurityConfiguration @Bean fun jwtAuthenticationFilter(): JwtAuthenticationFilter = - JwtAuthenticationFilter(tokenValidator, authenticationManager(), userRepository) + JwtAuthenticationFilter(tokenValidator, authenticationManager()) .skipUrlPattern(HttpMethod.GET, "/management/health") .skipUrlPattern(HttpMethod.POST, "/oauth/token") .skipUrlPattern(HttpMethod.GET, "/api/meta-token/*") diff --git a/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt index 9679cd9cc..e665cfed1 100644 --- a/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt +++ b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt @@ -1,249 +1,175 @@ package org.radarbase.management.security import io.ktor.http.* +import java.io.IOException +import java.time.Instant +import javax.annotation.Nonnull +import javax.servlet.FilterChain +import javax.servlet.ServletException +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse +import javax.servlet.http.HttpSession import org.radarbase.auth.authentication.TokenValidator -import org.radarbase.auth.authorization.AuthorityReference -import org.radarbase.auth.authorization.RoleAuthority import org.radarbase.auth.exception.TokenValidationException import org.radarbase.auth.token.RadarToken -import org.radarbase.management.domain.Role -import org.radarbase.management.domain.User -import org.radarbase.management.repository.UserRepository -import org.radarbase.management.web.rest.util.HeaderUtil.parseCookies import org.slf4j.LoggerFactory import org.springframework.http.HttpHeaders import org.springframework.http.HttpMethod import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.core.Authentication import org.springframework.security.core.context.SecurityContextHolder -import org.springframework.security.oauth2.provider.OAuth2Authentication import org.springframework.security.web.util.matcher.AntPathRequestMatcher import org.springframework.web.cors.CorsUtils import org.springframework.web.filter.OncePerRequestFilter -import java.io.IOException -import java.time.Instant -import javax.annotation.Nonnull -import javax.servlet.FilterChain -import javax.servlet.ServletException -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse -import javax.servlet.http.HttpSession - -/** - * Authentication filter using given validator. - * @param validator validates the JWT token. - * @param authenticationManager authentication manager to pass valid authentication to. - * @param userRepository user repository to retrieve user details from. - * @param isOptional do not fail if no authentication is provided - */ -class JwtAuthenticationFilter @JvmOverloads constructor( - private val validator: TokenValidator, - private val authenticationManager: AuthenticationManager, - private val userRepository: UserRepository, - private val isOptional: Boolean = false +class JwtAuthenticationFilter( + private val validator: TokenValidator, + private val authenticationManager: AuthenticationManager, + private val isOptional: Boolean = false ) : OncePerRequestFilter() { + private val ignoreUrls: MutableList = mutableListOf() - /** - * Do not use JWT authentication for given paths and HTTP method. - * @param method HTTP method - * @param antPatterns Ant wildcard pattern - * @return the current filter - */ fun skipUrlPattern(method: HttpMethod, vararg antPatterns: String?): JwtAuthenticationFilter { - for (pattern in antPatterns) { - ignoreUrls.add(AntPathRequestMatcher(pattern, method.name)) + antPatterns.forEach { pattern -> + pattern?.let { ignoreUrls.add(AntPathRequestMatcher(it, method.name)) } } return this } @Throws(IOException::class, ServletException::class) override fun doFilterInternal( - httpRequest: HttpServletRequest, - httpResponse: HttpServletResponse, - chain: FilterChain, + httpRequest: HttpServletRequest, + httpResponse: HttpServletResponse, + chain: FilterChain, ) { - Companion.logger.warn("Request: {}", httpServletRequestToString(httpRequest)) + logger.debug("Processing request: ${httpRequest.requestURI}") + if (CorsUtils.isPreFlightRequest(httpRequest)) { - Companion.logger.debug("Skipping JWT check for preflight request") + logger.debug("Skipping JWT check for preflight request") chain.doFilter(httpRequest, httpResponse) return } val existingAuthentication = SecurityContextHolder.getContext().authentication + val stringToken = tokenFromHeader(httpRequest) + var token: RadarToken? = null - if (existingAuthentication.isAnonymous || existingAuthentication is OAuth2Authentication) { - val session = httpRequest.getSession(false) - val stringToken = tokenFromHeader(httpRequest) - var token: RadarToken? = null - var exMessage = "No token provided" - if (stringToken != null) { - try { - Companion.logger.warn("Validating token from header: {}", stringToken) - token = validator.validateBlocking(stringToken) - Companion.logger.debug("Using token from header") - } catch (ex: TokenValidationException) { - ex.message?.let { exMessage = it } - Companion.logger.info("Failed to validate token from header: {}", exMessage) - } - } - if (token == null) { - token = session?.radarToken - ?.takeIf { Instant.now() < it.expiresAt } - if (token != null) { - Companion.logger.debug("Using token from session") - } - } - if (!validateToken(token, httpRequest, httpResponse, session, exMessage)) { - return - } + if (stringToken != null) { + token = validateTokenFromHeader(stringToken, httpRequest) + } + + if (token == null && existingAuthentication.isAnonymous) { + token = validateTokenFromSession(httpRequest.session) } - chain.doFilter(httpRequest, httpResponse) - } - private fun httpServletRequestToString(httpRequest: HttpServletRequest):String { - val buffer = StringBuffer() - buffer.append("Method: ${httpRequest.method}\n") - buffer.append("RequestURI: ${httpRequest.requestURI}\n") - buffer.append("QueryString: ${httpRequest.queryString}\n") - buffer.append("RequestURL: ${httpRequest.requestURL}\n") - buffer.append("Protocol: ${httpRequest.protocol}\n") - buffer.append("Scheme: ${httpRequest.scheme}\n") - buffer.append("ServerName: ${httpRequest.serverName}\n") - buffer.append("ServerPort: ${httpRequest.serverPort}\n") - buffer.append("RemoteAddr: ${httpRequest.remoteAddr}\n") - buffer.append("RemoteHost: ${httpRequest.remoteHost}\n") - buffer.append("RemotePort: ${httpRequest.remotePort}\n") - buffer.append("LocalAddr: ${httpRequest.localAddr}\n") - buffer.append("LocalName: ${httpRequest.localName}\n") - buffer.append("LocalPort: ${httpRequest.localPort}\n") - buffer.append("AuthType: ${httpRequest.authType}\n") - buffer.append("ContentType: ${httpRequest.contentType}\n") - buffer.append("ContentLength: ${httpRequest.contentLength}\n") - buffer.append("CharacterEncoding: ${httpRequest.characterEncoding}\n") - buffer.append("Cookies: ${httpRequest.cookies}\n") - buffer.append("Headers: ${httpRequest.headerNames.toList().map { it to httpRequest.getHeaders(it).toList() }}\n") - buffer.append("Attributes: ${httpRequest.attributeNames.toList().map { it to httpRequest.getAttribute(it) }}\n") - buffer.append("Parameters: ${httpRequest.parameterMap.toList().map { it.first to it.second.toList() }}\n") - return buffer.toString() + if (!validateAndSetAuthentication(token, httpRequest, httpResponse)) { + return + } + + chain.doFilter(httpRequest, httpResponse) } - override fun shouldNotFilter(@Nonnull httpRequest: HttpServletRequest): Boolean { - val shouldNotFilterUrl = ignoreUrls.find { it.matches(httpRequest) } - return if (shouldNotFilterUrl != null) { - Companion.logger.debug("Skipping JWT check for {} request", shouldNotFilterUrl) - true - } else { - false + private fun validateTokenFromHeader( + tokenString: String, + httpRequest: HttpServletRequest + ): RadarToken? { + return try { + logger.debug("Validating token from header: ${tokenString}") + val token = validator.validateBlocking(tokenString) + val authentication = createAuthenticationFromToken(token) + SecurityContextHolder.getContext().authentication = authentication + logger.debug("JWT authentication successful") + token + } catch (ex: TokenValidationException) { + logger.warn("Token validation failed: ${ex.message}") + null } } - private fun tokenFromHeader(httpRequest: HttpServletRequest): String? { - Companion.logger.warn("Token from header: {}", httpRequest.getHeader(HttpHeaders.AUTHORIZATION)) - return httpRequest.getHeader(HttpHeaders.AUTHORIZATION) - ?.takeIf { it.startsWith(AUTHORIZATION_BEARER_HEADER) } - ?.removePrefix(AUTHORIZATION_BEARER_HEADER) - ?.trim { it <= ' ' } - ?: parseCookies(httpRequest.getHeader(HttpHeaders.COOKIE)).find { it.name == "ory_kratos_session" } - ?.value + private fun validateTokenFromSession(session: HttpSession?): RadarToken? { + val token = session?.radarToken?.takeIf { Instant.now() < it.expiresAt } + if (token != null) { + logger.debug("Using token from session") + val authentication = createAuthenticationFromToken(token) + SecurityContextHolder.getContext().authentication = authentication + } + return token } - @Throws(IOException::class) - private fun validateToken( - token: RadarToken?, - httpRequest: HttpServletRequest, - httpResponse: HttpServletResponse, - session: HttpSession?, - exMessage: String?, + private fun validateAndSetAuthentication( + token: RadarToken?, + httpRequest: HttpServletRequest, + httpResponse: HttpServletResponse ): Boolean { return if (token != null) { - val updatedToken = checkUser(token, httpRequest, httpResponse, session) - ?: return false - httpRequest.radarToken = updatedToken - val authentication = RadarAuthentication(updatedToken) - authenticationManager.authenticate(authentication) + httpRequest.radarToken = token + val authentication = createAuthenticationFromToken(token) SecurityContextHolder.getContext().authentication = authentication true - } else if (isOptional) { - logger.debug("Skipping optional token") - true } else { - logger.error("Unauthorized - no valid token provided") - httpResponse.returnUnauthorized(httpRequest, exMessage) + handleUnauthorized(httpRequest, httpResponse, "No valid token provided") false } } - @Throws(IOException::class) - private fun checkUser( - token: RadarToken, - httpRequest: HttpServletRequest, - httpResponse: HttpServletResponse, - session: HttpSession?, - ): RadarToken? { - val userName = token.username ?: return token - val user = userRepository.findOneByLogin(userName) - return if (user != null) { - token.copyWithRoles(user.authorityReferences) - } else { - session?.removeAttribute(TOKEN_ATTRIBUTE) - httpResponse.returnUnauthorized(httpRequest, "User not found") - null + private fun handleUnauthorized( + httpRequest: HttpServletRequest, + httpResponse: HttpServletResponse, + message: String + ) { + if (!isOptional) { + logger.error("Unauthorized - ${message}") + httpResponse.returnUnauthorized(httpRequest, message) + } + } + + private fun createAuthenticationFromToken(token: RadarToken): Authentication { + val authentication = RadarAuthentication(token) + return authenticationManager.authenticate(authentication) + } + + override fun shouldNotFilter(@Nonnull httpRequest: HttpServletRequest): Boolean { + return ignoreUrls.any { it.matches(httpRequest) }.also { shouldSkip -> + if (shouldSkip) { + logger.debug("Skipping JWT check for ${httpRequest.requestURL}") + } } } + private fun tokenFromHeader(httpRequest: HttpServletRequest): String? { + return httpRequest + .getHeader(HttpHeaders.AUTHORIZATION) + ?.takeIf { it.startsWith(AUTHORIZATION_BEARER_HEADER) } + ?.removePrefix(AUTHORIZATION_BEARER_HEADER) + ?.trim() + } + companion object { - private fun HttpServletResponse.returnUnauthorized(request: HttpServletRequest, message: String?) { + private val logger = LoggerFactory.getLogger(JwtAuthenticationFilter::class.java) + private const val AUTHORIZATION_BEARER_HEADER = "Bearer" + private const val TOKEN_ATTRIBUTE = "jwt" + + private fun HttpServletResponse.returnUnauthorized( + request: HttpServletRequest, + message: String? + ) { status = HttpServletResponse.SC_UNAUTHORIZED setHeader(HttpHeaders.WWW_AUTHENTICATE, AUTHORIZATION_BEARER_HEADER) - val fullMessage = if (message != null) { - "\"$message\"" - } else { - "null" - } outputStream.print( - """ + """ {"error": "Unauthorized", "status": "${HttpServletResponse.SC_UNAUTHORIZED}", - message": $fullMessage, + "message": "${message ?: "null"}", "path": "${request.requestURI}"} """.trimIndent() ) } - private val logger = LoggerFactory.getLogger(JwtAuthenticationFilter::class.java) - private const val AUTHORIZATION_BEARER_HEADER = "Bearer" - private const val TOKEN_ATTRIBUTE = "jwt" - private const val TOKEN_COOKIE_NAME = "ory_kratos_session" - - /** - * Authority references for given user. The user should have its roles mapped - * from the database. - * @return set of authority references. - */ - val User.authorityReferences: Set - get() = roles.mapTo(HashSet()) { role: Role? -> - val auth = role?.role - val referent = when (auth?.scope) { - RoleAuthority.Scope.GLOBAL -> null - RoleAuthority.Scope.ORGANIZATION -> role.organization?.name - RoleAuthority.Scope.PROJECT -> role.project?.projectName - null -> null - } - AuthorityReference(auth!!, referent) - } - - - - @get:JvmStatic - @set:JvmStatic var HttpSession.radarToken: RadarToken? get() = getAttribute(TOKEN_ATTRIBUTE) as RadarToken? set(value) = setAttribute(TOKEN_ATTRIBUTE, value) - @get:JvmStatic - @set:JvmStatic var HttpServletRequest.radarToken: RadarToken? get() = getAttribute(TOKEN_ATTRIBUTE) as RadarToken? set(value) = setAttribute(TOKEN_ATTRIBUTE, value) @@ -252,7 +178,7 @@ class JwtAuthenticationFilter @JvmOverloads constructor( get() { this ?: return true return authorities.size == 1 && - authorities.firstOrNull()?.authority == "ROLE_ANONYMOUS" + authorities.firstOrNull()?.authority == "ROLE_ANONYMOUS" } } } diff --git a/src/test/java/org/radarbase/auth/authentication/OAuthHelper.kt b/src/test/java/org/radarbase/auth/authentication/OAuthHelper.kt index 0ab493a0f..5aa0cd8e7 100644 --- a/src/test/java/org/radarbase/auth/authentication/OAuthHelper.kt +++ b/src/test/java/org/radarbase/auth/authentication/OAuthHelper.kt @@ -128,7 +128,7 @@ object OAuthHelper { Mockito.`when`(userRepository.findOneByLogin(ArgumentMatchers.anyString())).thenReturn( createAdminUser() ) - return JwtAuthenticationFilter(createTokenValidator(), { auth: Authentication? -> auth }, userRepository) + return JwtAuthenticationFilter(createTokenValidator(), { auth: Authentication? -> auth }) } /** From 54aa7ff981a80c88b09d7e5c1157b566de9d389a Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 22 Aug 2024 23:58:42 +0100 Subject: [PATCH 15/22] Remove unused services --- build.gradle | 4 +- ...ManagementPortalJwtAccessTokenConverter.kt | 251 ----------------- .../jwt/ManagementPortalJwtTokenStore.kt | 189 ------------- .../management/service/MetaTokenService.kt | 252 ------------------ .../management/service/OAuthClientService.kt | 175 ------------ .../decorator/ProjectMapperDecorator.kt | 7 - .../management/web/rest/MetaTokenResource.kt | 91 ------- .../web/rest/OAuthClientsResource.kt | 227 ---------------- .../auth/authentication/OAuthHelper.kt | 6 +- .../service/MetaTokenServiceTest.kt | 136 ---------- .../web/rest/OAuthClientsResourceIntTest.kt | 212 --------------- 11 files changed, 4 insertions(+), 1546 deletions(-) delete mode 100644 src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt delete mode 100644 src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtTokenStore.kt delete mode 100644 src/main/java/org/radarbase/management/service/MetaTokenService.kt delete mode 100644 src/main/java/org/radarbase/management/service/OAuthClientService.kt delete mode 100644 src/main/java/org/radarbase/management/web/rest/MetaTokenResource.kt delete mode 100644 src/main/java/org/radarbase/management/web/rest/OAuthClientsResource.kt delete mode 100644 src/test/java/org/radarbase/management/service/MetaTokenServiceTest.kt delete mode 100644 src/test/java/org/radarbase/management/web/rest/OAuthClientsResourceIntTest.kt diff --git a/build.gradle b/build.gradle index 83c1dbf69..b76d738a9 100644 --- a/build.gradle +++ b/build.gradle @@ -175,7 +175,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-autoconfigure") implementation "org.springframework.boot:spring-boot-starter-mail" runtimeOnly "org.springframework.boot:spring-boot-starter-logging" - runtimeOnly ("org.springframework.boot:spring-boot-starter-data-jpa") { + runtimeOnly("org.springframework.boot:spring-boot-starter-data-jpa") { exclude group: 'org.hibernate', module: 'hibernate-entitymanager' } implementation "org.springframework.security:spring-security-data" @@ -184,7 +184,7 @@ dependencies { exclude module: 'spring-boot-starter-tomcat' } runtimeOnly "org.springframework.boot:spring-boot-starter-security" - implementation ("org.springframework.boot:spring-boot-starter-undertow") + implementation("org.springframework.boot:spring-boot-starter-undertow") implementation "org.hibernate:hibernate-core" implementation "org.hibernate:hibernate-envers" diff --git a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt deleted file mode 100644 index 7a997ba36..000000000 --- a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt +++ /dev/null @@ -1,251 +0,0 @@ -package org.radarbase.management.security.jwt - -import com.auth0.jwt.JWT -import com.auth0.jwt.JWTVerifier -import com.auth0.jwt.algorithms.Algorithm -import com.auth0.jwt.exceptions.JWTVerificationException -import com.auth0.jwt.exceptions.SignatureVerificationException -import com.fasterxml.jackson.core.JsonProcessingException -import com.fasterxml.jackson.databind.ObjectMapper -import org.radarbase.management.security.jwt.ManagementPortalJwtAccessTokenConverter -import org.radarbase.auth.authentication.TokenValidator -import org.slf4j.LoggerFactory -import org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken -import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken -import org.springframework.security.oauth2.common.DefaultOAuth2RefreshToken -import org.springframework.security.oauth2.common.ExpiringOAuth2RefreshToken -import org.springframework.security.oauth2.common.OAuth2AccessToken -import org.springframework.security.oauth2.common.exceptions.InvalidTokenException -import org.springframework.security.oauth2.provider.OAuth2Authentication -import org.springframework.security.oauth2.provider.token.AccessTokenConverter -import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter -import org.springframework.security.oauth2.provider.token.store.JwtClaimsSetVerifier -import org.springframework.util.Assert -import java.nio.charset.StandardCharsets -import java.time.Instant -import java.util.* -import java.util.stream.Stream -import org.radarbase.auth.exception.TokenValidationException - -/** - * Implementation of [JwtAccessTokenConverter] for the RADAR-base ManagementPortal platform. - * - * - * This class can accept an EC keypair as well as an RSA keypair for signing. EC signatures - * are significantly smaller than RSA signatures. - */ -open class ManagementPortalJwtAccessTokenConverter( - validator: TokenValidator, -// algorithm: Algorithm, -// verifiers: MutableList, -// private val refreshTokenVerifiers: List -) : JwtAccessTokenConverter { - private val jsonParser = ObjectMapper().readerFor( - MutableMap::class.java - ) - private val tokenConverter: AccessTokenConverter - - /** - * Returns JwtClaimsSetVerifier. - * - * @return the [JwtClaimsSetVerifier] used to verify the claim(s) in the JWT Claims Set - */ - var jwtClaimsSetVerifier: JwtClaimsSetVerifier? = null - /** - * Sets JwtClaimsSetVerifier instance. - * - * @param jwtClaimsSetVerifier the [JwtClaimsSetVerifier] used to verify the claim(s) - * in the JWT Claims Set - */ - set(jwtClaimsSetVerifier) { - Assert.notNull(jwtClaimsSetVerifier, "jwtClaimsSetVerifier cannot be null") - field = jwtClaimsSetVerifier - } - private var algorithm: Algorithm? = null - private var validator: TokenValidator - //private val verifiers: MutableList - - /** - * Default constructor. - * Creates [ManagementPortalJwtAccessTokenConverter] with - * [DefaultAccessTokenConverter] as the accessTokenConverter with explicitly including - * grant_type claim. - */ - init { - val accessToken = DefaultAccessTokenConverter() - accessToken.setIncludeGrantType(true) - tokenConverter = accessToken - //this.verifiers = verifiers - this.validator = validator - //setAlgorithm(algorithm) - } - - override fun convertAccessToken( - token: OAuth2AccessToken, - authentication: OAuth2Authentication - ): Map { - return tokenConverter.convertAccessToken(token, authentication) - } - - override fun extractAccessToken(value: String, map: Map?): OAuth2AccessToken { - var mapCopy = map?.toMutableMap() - - if (mapCopy?.containsKey(AccessTokenConverter.EXP) == true) { - mapCopy[AccessTokenConverter.EXP] = (mapCopy[AccessTokenConverter.EXP] as Int).toLong() - } - return tokenConverter.extractAccessToken(value, mapCopy) - } - - override fun extractAuthentication(map: Map?): OAuth2Authentication { - return tokenConverter.extractAuthentication(map) - } - - override fun setAlgorithm(algorithm: Algorithm) { - this.algorithm = algorithm -// if (verifiers.isEmpty()) { -// verifiers.add(JWT.require(algorithm).withAudience(RES_MANAGEMENT_PORTAL).build()) -// } - } - - /** - * Simplified the existing enhancing logic of - * [JwtAccessTokenConverter.enhance]. - * Keeping the same logic. - * - * - * - * It mainly adds token-id for access token and access-token-id and token-id for refresh - * token to the additional information. - * - * - * @param accessToken accessToken to enhance. - * @param authentication current authentication of the token. - * @return enhancedToken. - */ - override fun enhance( - accessToken: OAuth2AccessToken, - authentication: OAuth2Authentication - ): OAuth2AccessToken { - // create new instance of token to enhance - val resultAccessToken = DefaultOAuth2AccessToken(accessToken) - // set additional information for access token - val additionalInfoAccessToken: MutableMap = HashMap(accessToken.additionalInformation) - - // add token id if not available - var accessTokenId = accessToken.value - if (!additionalInfoAccessToken.containsKey(JwtAccessTokenConverter.TOKEN_ID)) { - additionalInfoAccessToken[JwtAccessTokenConverter.TOKEN_ID] = accessTokenId - } else { - accessTokenId = additionalInfoAccessToken[JwtAccessTokenConverter.TOKEN_ID] as String? - } - resultAccessToken.additionalInformation = additionalInfoAccessToken - resultAccessToken.value = encode(accessToken, authentication) - - // add additional information for refresh-token - val refreshToken = accessToken.refreshToken - if (refreshToken != null) { - val refreshTokenToEnhance = DefaultOAuth2AccessToken(accessToken) - refreshTokenToEnhance.value = refreshToken.value - // Refresh tokens do not expire unless explicitly of the right type - refreshTokenToEnhance.expiration = null - refreshTokenToEnhance.scope = accessToken.scope - // set info of access token to refresh-token and add token-id and access-token-id for - // reference. - val refreshTokenInfo: MutableMap = HashMap(accessToken.additionalInformation) - refreshTokenInfo[JwtAccessTokenConverter.TOKEN_ID] = refreshTokenToEnhance.value - refreshTokenInfo[JwtAccessTokenConverter.ACCESS_TOKEN_ID] = accessTokenId - refreshTokenToEnhance.additionalInformation = refreshTokenInfo - val encodedRefreshToken: DefaultOAuth2RefreshToken - if (refreshToken is ExpiringOAuth2RefreshToken) { - val expiration = refreshToken.expiration - refreshTokenToEnhance.expiration = expiration - encodedRefreshToken = DefaultExpiringOAuth2RefreshToken( - encode(refreshTokenToEnhance, authentication), expiration - ) - } else { - encodedRefreshToken = DefaultOAuth2RefreshToken( - encode(refreshTokenToEnhance, authentication) - ) - } - resultAccessToken.refreshToken = encodedRefreshToken - } - return resultAccessToken - } - - override fun isRefreshToken(token: OAuth2AccessToken): Boolean { - return token.additionalInformation?.containsKey(JwtAccessTokenConverter.ACCESS_TOKEN_ID) == true - } - - override fun encode(accessToken: OAuth2AccessToken, authentication: OAuth2Authentication): String { - // we need to override the encode method as well, Spring security does not know about - // ECDSA, so it can not set the 'alg' header claim of the JWT to the correct value; here - // we use the auth0 JWT implementation to create a signed, encoded JWT. - val claims = convertAccessToken(accessToken, authentication) - val builder = JWT.create() - - // add the string array claims - Stream.of("aud", "sources", "roles", "authorities", "scope") - .filter { key: String -> claims.containsKey(key) } - .forEach { claim: String -> - builder.withArrayClaim( - claim, - (claims[claim] as Collection).toTypedArray() - ) - } - - // add the string claims - Stream.of("sub", "iss", "user_name", "client_id", "grant_type", "jti", "ati") - .filter { key: String -> claims.containsKey(key) } - .forEach { claim: String -> builder.withClaim(claim, claims[claim] as String?) } - - // add the date claims, they are in seconds since epoch, we need milliseconds - Stream.of("exp", "iat") - .filter { key: String -> claims.containsKey(key) } - .forEach { claim: String -> - builder.withClaim( - claim, - Date.from(Instant.ofEpochSecond((claims[claim] as Long?)!!)) - ) - } - return builder.sign(algorithm) - } - - override fun decode(token: String): Map { - val jwt = JWT.decode(token) -// val verifierToUse: List - val claims: MutableMap - try { - val decodedPayload = String( - Base64.getUrlDecoder().decode(jwt.payload), - StandardCharsets.UTF_8 - ) - claims = jsonParser.readValue(decodedPayload) - if (claims.containsKey(AccessTokenConverter.EXP) && claims[AccessTokenConverter.EXP] is Int) { - val intValue = claims[AccessTokenConverter.EXP] as Int? - claims[AccessTokenConverter.EXP] = intValue!! - } - if (jwtClaimsSetVerifier != null) { - jwtClaimsSetVerifier!!.verify(claims) - } -// verifierToUse = -// if (claims[JwtAccessTokenConverter.ACCESS_TOKEN_ID] != null) refreshTokenVerifiers else verifiers - } catch (ex: JsonProcessingException) { - throw InvalidTokenException("Invalid token", ex) - } - try { - validator.validateBlocking(token) - logger.debug("Using token from header") - return claims - } catch (ex: TokenValidationException) { - ex.message?.let { - logger.info("Failed to validate token from header: {}", it) - } - } - throw InvalidTokenException("No registered validator could authenticate this token") - } - - companion object { - const val RES_MANAGEMENT_PORTAL = "res_ManagementPortal" - private val logger = LoggerFactory.getLogger(ManagementPortalJwtAccessTokenConverter::class.java) - } -} diff --git a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtTokenStore.kt b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtTokenStore.kt deleted file mode 100644 index 512b66433..000000000 --- a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtTokenStore.kt +++ /dev/null @@ -1,189 +0,0 @@ -package org.radarbase.management.security.jwt - -import org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken -import org.springframework.security.oauth2.common.DefaultOAuth2RefreshToken -import org.springframework.security.oauth2.common.OAuth2AccessToken -import org.springframework.security.oauth2.common.OAuth2RefreshToken -import org.springframework.security.oauth2.common.exceptions.InvalidTokenException -import org.springframework.security.oauth2.provider.OAuth2Authentication -import org.springframework.security.oauth2.provider.approval.Approval -import org.springframework.security.oauth2.provider.approval.Approval.ApprovalStatus -import org.springframework.security.oauth2.provider.approval.ApprovalStore -import org.springframework.security.oauth2.provider.token.TokenStore -import java.util.* - -/** - * Adapted version of [org.springframework.security.oauth2.provider.token.store.JwtTokenStore] - * which uses interface [JwtAccessTokenConverter] instead of tied instance. - * - * - * - * A [TokenStore] implementation that just reads data from the tokens themselves. - * Not really a store since it never persists anything, and methods like - * [.getAccessToken] always return null. But - * nevertheless a useful tool since it translates access tokens to and - * from authentications. Use this wherever a[TokenStore] is needed, - * but remember to use the same [JwtAccessTokenConverter] - * instance (or one with the same verifier) as was used when the tokens were minted. - * - * - * @author Dave Syer - * @author nivethika - */ -class ManagementPortalJwtTokenStore : TokenStore { - private val jwtAccessTokenConverter: JwtAccessTokenConverter - private var approvalStore: ApprovalStore? = null - - /** - * Create a ManagementPortalJwtTokenStore with this token converter - * (should be shared with the DefaultTokenServices if used). - * - * @param jwtAccessTokenConverter JwtAccessTokenConverter used in the application. - */ - constructor(jwtAccessTokenConverter: JwtAccessTokenConverter) { - this.jwtAccessTokenConverter = jwtAccessTokenConverter - } - - /** - * Create a ManagementPortalJwtTokenStore with this token converter - * (should be shared with the DefaultTokenServices if used). - * - * @param jwtAccessTokenConverter JwtAccessTokenConverter used in the application. - * @param approvalStore TokenApprovalStore used in the application. - */ - constructor( - jwtAccessTokenConverter: JwtAccessTokenConverter, - approvalStore: ApprovalStore? - ) { - this.jwtAccessTokenConverter = jwtAccessTokenConverter - this.approvalStore = approvalStore - } - - /** - * ApprovalStore to be used to validate and restrict refresh tokens. - * - * @param approvalStore the approvalStore to set - */ - fun setApprovalStore(approvalStore: ApprovalStore?) { - this.approvalStore = approvalStore - } - - override fun readAuthentication(token: OAuth2AccessToken): OAuth2Authentication { - return readAuthentication(token.value) - } - - override fun readAuthentication(token: String): OAuth2Authentication { - return jwtAccessTokenConverter.extractAuthentication(jwtAccessTokenConverter.decode(token)) - } - - override fun storeAccessToken(token: OAuth2AccessToken, authentication: OAuth2Authentication) { - // this is not really a store where we persist - } - - override fun readAccessToken(tokenValue: String): OAuth2AccessToken { - val accessToken = convertAccessToken(tokenValue) - if (jwtAccessTokenConverter.isRefreshToken(accessToken)) { - throw InvalidTokenException("Encoded token is a refresh token") - } - return accessToken - } - - private fun convertAccessToken(tokenValue: String): OAuth2AccessToken { - return jwtAccessTokenConverter - .extractAccessToken(tokenValue, jwtAccessTokenConverter.decode(tokenValue)) - } - - override fun removeAccessToken(token: OAuth2AccessToken) { - // this is not really store where we persist - } - - override fun storeRefreshToken( - refreshToken: OAuth2RefreshToken, - authentication: OAuth2Authentication - ) { - // this is not really store where we persist - } - - override fun readRefreshToken(tokenValue: String): OAuth2RefreshToken? { - if (approvalStore != null) { - val authentication = readAuthentication(tokenValue) - if (authentication.userAuthentication != null) { - val userId = authentication.userAuthentication.name - val clientId = authentication.oAuth2Request.clientId - val approvals = approvalStore!!.getApprovals(userId, clientId) - val approvedScopes: MutableCollection = HashSet() - for (approval in approvals) { - if (approval.isApproved) { - approvedScopes.add(approval.scope) - } - } - if (!approvedScopes.containsAll(authentication.oAuth2Request.scope)) { - return null - } - } - } - val encodedRefreshToken = convertAccessToken(tokenValue) - return createRefreshToken(encodedRefreshToken) - } - - private fun createRefreshToken(encodedRefreshToken: OAuth2AccessToken): OAuth2RefreshToken { - if (!jwtAccessTokenConverter.isRefreshToken(encodedRefreshToken)) { - throw InvalidTokenException("Encoded token is not a refresh token") - } - return if (encodedRefreshToken.expiration != null) { - DefaultExpiringOAuth2RefreshToken( - encodedRefreshToken.value, - encodedRefreshToken.expiration - ) - } else DefaultOAuth2RefreshToken(encodedRefreshToken.value) - } - - override fun readAuthenticationForRefreshToken(token: OAuth2RefreshToken): OAuth2Authentication { - return readAuthentication(token.value) - } - - override fun removeRefreshToken(token: OAuth2RefreshToken) { - remove(token.value) - } - - private fun remove(token: String) { - if (approvalStore != null) { - val auth = readAuthentication(token) - val clientId = auth.oAuth2Request.clientId - val user = auth.userAuthentication - if (user != null) { - val approvals: MutableCollection = ArrayList() - for (scope in auth.oAuth2Request.scope) { - approvals.add( - Approval( - user.name, clientId, scope, Date(), - ApprovalStatus.APPROVED - ) - ) - } - approvalStore!!.revokeApprovals(approvals) - } - } - } - - override fun removeAccessTokenUsingRefreshToken(refreshToken: OAuth2RefreshToken) { - // this is not really store where we persist - } - - override fun getAccessToken(authentication: OAuth2Authentication): OAuth2AccessToken? { - // We don't want to accidentally issue a token, and we have no way to reconstruct - // the refresh token - return null - } - - override fun findTokensByClientIdAndUserName( - clientId: String, - userName: String - ): Collection { - return emptySet() - } - - override fun findTokensByClientId(clientId: String): Collection { - return emptySet() - } -} diff --git a/src/main/java/org/radarbase/management/service/MetaTokenService.kt b/src/main/java/org/radarbase/management/service/MetaTokenService.kt deleted file mode 100644 index dcbdb0e3d..000000000 --- a/src/main/java/org/radarbase/management/service/MetaTokenService.kt +++ /dev/null @@ -1,252 +0,0 @@ -package org.radarbase.management.service - -import org.radarbase.management.config.ManagementPortalProperties -import org.radarbase.management.domain.MetaToken -import org.radarbase.management.domain.Project -import org.radarbase.management.domain.Subject -import org.radarbase.management.repository.MetaTokenRepository -import org.radarbase.management.security.NotAuthorizedException -import org.radarbase.management.service.dto.ClientPairInfoDTO -import org.radarbase.management.service.dto.TokenDTO -import org.radarbase.management.web.rest.MetaTokenResource -import org.radarbase.management.web.rest.errors.BadRequestException -import org.radarbase.management.web.rest.errors.EntityName -import org.radarbase.management.web.rest.errors.ErrorConstants -import org.radarbase.management.web.rest.errors.InvalidStateException -import org.radarbase.management.web.rest.errors.NotFoundException -import org.radarbase.management.web.rest.errors.RequestGoneException -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.scheduling.annotation.Scheduled -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional -import java.net.MalformedURLException -import java.net.URISyntaxException -import java.net.URL -import java.time.Duration -import java.time.Instant -import java.time.format.DateTimeParseException -import java.util.* -import java.util.function.Consumer -import javax.validation.ConstraintViolationException - -/** - * Created by nivethika. - * - * - * Service to delegate MetaToken handling. - * - */ -//@Service -//@Transactional -class MetaTokenService { - @Autowired - private val metaTokenRepository: MetaTokenRepository? = null - - @Autowired - private val oAuthClientService: OAuthClientService? = null - - @Autowired - private val managementPortalProperties: ManagementPortalProperties? = null - - @Autowired - private val subjectService: SubjectService? = null - - /** - * Save a metaToken. - * - * @param metaToken the entity to save - * @return the persisted entity - */ - fun save(metaToken: MetaToken): MetaToken { - log.debug("Request to save MetaToken : {}", metaToken) - return metaTokenRepository!!.save(metaToken) - } - - /** - * Get one project by id. - * - * @param tokenName the id of the entity - * @return the entity - */ - @Throws(MalformedURLException::class) - fun fetchToken(tokenName: String): TokenDTO { - log.debug("Request to get Token : {}", tokenName) - val metaToken = getToken(tokenName) - // process the response if the token is not fetched or not expired - return if (metaToken.isValid) { - val refreshToken = oAuthClientService!!.createAccessToken( - metaToken.subject!!.user!!, - metaToken.clientId!! - ) - .refreshToken - .value - - // create response - val result = TokenDTO( - refreshToken, - URL(managementPortalProperties!!.common.baseUrl), - subjectService!!.getPrivacyPolicyUrl(metaToken.subject!!) - ) - - // change fetched status to true. - if (!metaToken.isFetched()) { - metaToken.fetched(true) - save(metaToken) - } - result - } else { - throw RequestGoneException( - "Token $tokenName already fetched or expired. ", - EntityName.META_TOKEN, "error.TokenCannotBeSent" - ) - } - } - - /** - * Gets a token from databased using the tokenName. - * - * @param tokenName tokenName. - * @return fetched token as [MetaToken]. - */ - @Transactional(readOnly = true) - fun getToken(tokenName: String): MetaToken { - return metaTokenRepository!!.findOneByTokenName(tokenName) - ?: throw NotFoundException( - "Meta token not found with tokenName", - EntityName.META_TOKEN, - ErrorConstants.ERR_TOKEN_NOT_FOUND, - Collections.singletonMap("tokenName", tokenName) - ) - } - - /** - * Saves a unique meta-token instance, by checking for token-name collision. - * If a collision is detection, we try to save the token with a new tokenName - * @return an unique token - */ - fun saveUniqueToken( - subject: Subject?, - clientId: String?, - fetched: Boolean?, - expiryTime: Instant?, - persistent: Boolean - ): MetaToken { - val metaToken = MetaToken() - .generateName(if (persistent) MetaToken.LONG_ID_LENGTH else MetaToken.SHORT_ID_LENGTH) - .fetched(fetched!!) - .expiryDate(expiryTime) - .subject(subject) - .clientId(clientId) - .persistent(persistent) - return try { - metaTokenRepository!!.save(metaToken) - } catch (e: ConstraintViolationException) { - log.warn("Unique constraint violation catched... Trying to save with new tokenName") - saveUniqueToken(subject, clientId, fetched, expiryTime, persistent) - } - } - - /** - * Creates meta token for oauth-subject pair. - * @param subject to create token for - * @param clientId using which client id - * @param persistent whether to persist the token after it is has been fetched - * @return [ClientPairInfoDTO] to return. - * @throws URISyntaxException when token URI cannot be formed properly. - * @throws MalformedURLException when token URL cannot be formed properly. - */ - @Throws(URISyntaxException::class, MalformedURLException::class, NotAuthorizedException::class) - fun createMetaToken(subject: Subject, clientId: String?, persistent: Boolean): ClientPairInfoDTO { - val timeout = getMetaTokenTimeout(persistent, project = subject.activeProject - ?:throw NotAuthorizedException("Cannot calculate meta-token duration without configured project") - ) - - // tokenName should be generated - val metaToken = saveUniqueToken( - subject, clientId, false, - Instant.now().plus(timeout), persistent - ) - val tokenName = metaToken.tokenName - return if (metaToken.id != null && tokenName != null) { - // get base url from settings - val baseUrl = managementPortalProperties!!.common.managementPortalBaseUrl - // create complete uri string - val tokenUrl = baseUrl + ResourceUriService.getUri(metaToken).getPath() - // create response - ClientPairInfoDTO( - URL(baseUrl), tokenName, - URL(tokenUrl), timeout - ) - } else { - throw InvalidStateException( - "Could not create a valid token", EntityName.OAUTH_CLIENT, - "error.couldNotCreateToken" - ) - } - } - - /** - * Gets the meta-token timeout from config file. If the config is not mentioned or in wrong - * format, it will return default value. - * - * @return meta-token timeout duration. - * @throws BadRequestException if a persistent token is requested but it is not configured. - */ - fun getMetaTokenTimeout(persistent: Boolean, project: Project?): Duration { - val timeoutConfig: String? - val defaultTimeout: Duration - if (persistent) { - timeoutConfig = managementPortalProperties!!.oauth.persistentMetaTokenTimeout - if (timeoutConfig == null || timeoutConfig.isEmpty()) { - throw BadRequestException( - "Cannot create persistent token: not supported in configuration.", - EntityName.META_TOKEN, ErrorConstants.ERR_PERSISTENT_TOKEN_DISABLED - ) - } - defaultTimeout = MetaTokenResource.DEFAULT_PERSISTENT_META_TOKEN_TIMEOUT - } else { - timeoutConfig = managementPortalProperties!!.oauth.metaTokenTimeout - defaultTimeout = MetaTokenResource.DEFAULT_META_TOKEN_TIMEOUT - if (timeoutConfig == null || timeoutConfig.isEmpty()) { - return defaultTimeout - } - } - return try { - Duration.parse(timeoutConfig) - } catch (e: DateTimeParseException) { - // if the token timeout cannot be read, log the error and use the default value. - log.warn( - "Cannot parse meta-token timeout config. Using default value {}", - defaultTimeout, e - ) - defaultTimeout - } - } - - /** - * Expired and fetched tokens are deleted after 1 month. - * - * This is scheduled to get triggered first day of the month. - */ - @Scheduled(cron = "0 0 0 1 * ?") - fun removeStaleTokens() { - log.info("Scheduled scan for expired and fetched meta-tokens starting now") - metaTokenRepository!!.findAllByFetchedOrExpired(Instant.now()) - .forEach(Consumer { metaToken: MetaToken -> - log.info( - "Deleting deleting expired or fetched token {}", - metaToken.tokenName - ) - metaTokenRepository.delete(metaToken) - }) - } - - fun delete(token: MetaToken) { - metaTokenRepository!!.delete(token) - } - - companion object { - private val log = LoggerFactory.getLogger(MetaTokenService::class.java) - } -} diff --git a/src/main/java/org/radarbase/management/service/OAuthClientService.kt b/src/main/java/org/radarbase/management/service/OAuthClientService.kt deleted file mode 100644 index 735d6a23f..000000000 --- a/src/main/java/org/radarbase/management/service/OAuthClientService.kt +++ /dev/null @@ -1,175 +0,0 @@ -package org.radarbase.management.service - -import org.radarbase.management.domain.User -import org.radarbase.management.service.dto.ClientDetailsDTO -import org.radarbase.management.service.mapper.ClientDetailsMapper -import org.radarbase.management.web.rest.errors.ConflictException -import org.radarbase.management.web.rest.errors.EntityName -import org.radarbase.management.web.rest.errors.ErrorConstants -import org.radarbase.management.web.rest.errors.InvalidRequestException -import org.radarbase.management.web.rest.errors.NotFoundException -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken -import org.springframework.security.core.Authentication -import org.springframework.security.core.authority.SimpleGrantedAuthority -import org.springframework.security.oauth2.common.OAuth2AccessToken -import org.springframework.security.oauth2.common.util.OAuth2Utils -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration -import org.springframework.security.oauth2.provider.ClientDetails -import org.springframework.security.oauth2.provider.NoSuchClientException -import org.springframework.security.oauth2.provider.OAuth2Authentication -import org.springframework.security.oauth2.provider.OAuth2Request -import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService -import org.springframework.stereotype.Service -import java.util.* - -/** - * The service layer to handle OAuthClient and Token related functions. - * Created by nivethika on 03/08/2018. - */ -//@Service -class OAuthClientService( - @Autowired private val clientDetailsService: JdbcClientDetailsService, - @Autowired private val clientDetailsMapper: ClientDetailsMapper, - @Autowired private val authorizationServerEndpointsConfiguration: AuthorizationServerEndpointsConfiguration -) { - - fun findAllOAuthClients(): List { - return clientDetailsService.listClientDetails() - } - - /** - * Find ClientDetails by OAuth client id. - * - * @param clientId The client ID to look up - * @return a ClientDetails object with the requested client ID - * @throws NotFoundException If there is no client with the requested ID - */ - fun findOneByClientId(clientId: String?): ClientDetails { - return try { - clientDetailsService.loadClientByClientId(clientId) - } catch (e: NoSuchClientException) { - log.error("Pair client request for unknown client id: {}", clientId) - val errorParams: MutableMap = HashMap() - errorParams["clientId"] = clientId - throw NotFoundException( - "Client not found for client-id", EntityName.Companion.OAUTH_CLIENT, - ErrorConstants.ERR_OAUTH_CLIENT_ID_NOT_FOUND, errorParams - ) - } - } - - /** - * Update Oauth-client with new information. - * - * @param clientDetailsDto information to update. - * @return Updated [ClientDetails] instance. - */ - fun updateOauthClient(clientDetailsDto: ClientDetailsDTO): ClientDetails { - val details: ClientDetails? = clientDetailsMapper.clientDetailsDTOToClientDetails(clientDetailsDto) - // update client. - clientDetailsService.updateClientDetails(details) - val updated = findOneByClientId(clientDetailsDto.clientId) - // updateClientDetails does not update secret, so check for it separately - if (clientDetailsDto.clientSecret != null && clientDetailsDto.clientSecret != updated.clientSecret) { - clientDetailsService.updateClientSecret( - clientDetailsDto.clientId, - clientDetailsDto.clientSecret - ) - } - return findOneByClientId(clientDetailsDto.clientId) - } - - /** - * Deletes an oauth client. - * @param clientId of the auth-client to delete. - */ - fun deleteClientDetails(clientId: String?) { - clientDetailsService.removeClientDetails(clientId) - } - - /** - * Creates new oauth-client. - * - * @param clientDetailsDto data to create oauth-client. - * @return created [ClientDetails]. - */ - fun createClientDetail(clientDetailsDto: ClientDetailsDTO): ClientDetails { - // check if the client id exists - try { - val existingClient = clientDetailsService.loadClientByClientId(clientDetailsDto.clientId) - if (existingClient != null) { - throw ConflictException( - "OAuth client already exists with this id", - EntityName.Companion.OAUTH_CLIENT, ErrorConstants.ERR_CLIENT_ID_EXISTS, - Collections.singletonMap("client_id", clientDetailsDto.clientId) - ) - } - } catch (ex: NoSuchClientException) { - // Client does not exist yet, we can go ahead and create it - log.info( - "No client existing with client-id {}. Proceeding to create new client", - clientDetailsDto.clientId - ) - } - val details: ClientDetails? = clientDetailsMapper.clientDetailsDTOToClientDetails(clientDetailsDto) - // create oauth client. - clientDetailsService.addClientDetails(details) - return findOneByClientId(clientDetailsDto.clientId) - } - - /** - * Internally creates an [OAuth2AccessToken] token using authorization-code flow. This - * method bypasses the usual authorization code flow mechanism, so it should only be used where - * appropriate, e.g., for subject impersonation. - * - * @param clientId oauth client id. - * @param user user of the token. - * @return Created [OAuth2AccessToken] instance. - */ - fun createAccessToken(user: User, clientId: String): OAuth2AccessToken { - val authorities = user.authorities!! - .map { a -> SimpleGrantedAuthority(a) } - // lookup the OAuth client - // getOAuthClient checks if the id exists - val client = findOneByClientId(clientId) - val requestParameters = Collections.singletonMap( - OAuth2Utils.GRANT_TYPE, "authorization_code" - ) - val responseTypes = setOf("code") - val oAuth2Request = OAuth2Request( - requestParameters, clientId, authorities, true, client.scope, - client.resourceIds, null, responseTypes, emptyMap() - ) - val authenticationToken: Authentication = UsernamePasswordAuthenticationToken( - user.login, null, authorities - ) - return authorizationServerEndpointsConfiguration.getEndpointsConfigurer() - .tokenServices - .createAccessToken(OAuth2Authentication(oAuth2Request, authenticationToken)) - } - - companion object { - private val log = LoggerFactory.getLogger(OAuthClientService::class.java) - private const val PROTECTED_KEY = "protected" - - /** - * Checks whether a client is a protected client. - * - * @param details ClientDetails. - */ - fun checkProtected(details: ClientDetails) { - val info = details.additionalInformation - if (Objects.nonNull(info) && info.containsKey(PROTECTED_KEY) && info[PROTECTED_KEY] - .toString().equals("true", ignoreCase = true) - ) { - throw InvalidRequestException( - "Cannot modify protected client", EntityName.Companion.OAUTH_CLIENT, - ErrorConstants.ERR_OAUTH_CLIENT_PROTECTED, - Collections.singletonMap("client_id", details.clientId) - ) - } - } - } -} diff --git a/src/main/java/org/radarbase/management/service/mapper/decorator/ProjectMapperDecorator.kt b/src/main/java/org/radarbase/management/service/mapper/decorator/ProjectMapperDecorator.kt index c51341b59..afcafb1d9 100644 --- a/src/main/java/org/radarbase/management/service/mapper/decorator/ProjectMapperDecorator.kt +++ b/src/main/java/org/radarbase/management/service/mapper/decorator/ProjectMapperDecorator.kt @@ -3,7 +3,6 @@ package org.radarbase.management.service.mapper.decorator import org.radarbase.management.domain.Project import org.radarbase.management.repository.OrganizationRepository import org.radarbase.management.repository.ProjectRepository -import org.radarbase.management.service.MetaTokenService import org.radarbase.management.service.dto.MinimalProjectDetailsDTO import org.radarbase.management.service.dto.ProjectDTO import org.radarbase.management.service.mapper.ProjectMapper @@ -23,16 +22,10 @@ abstract class ProjectMapperDecorator : ProjectMapper { @Autowired @Qualifier("delegate") private lateinit var delegate: ProjectMapper @Autowired private lateinit var organizationRepository: OrganizationRepository @Autowired private lateinit var projectRepository: ProjectRepository - //@Autowired private lateinit var metaTokenService: MetaTokenService override fun projectToProjectDTO(project: Project?): ProjectDTO? { val dto = delegate.projectToProjectDTO(project) dto?.humanReadableProjectName = project?.attributes?.get(ProjectDTO.HUMAN_READABLE_PROJECT_NAME) -// try { -// dto?.persistentTokenTimeout = metaTokenService.getMetaTokenTimeout(true, project).toMillis() -// } catch (ex: BadRequestException) { -// dto?.persistentTokenTimeout = null -// } return dto } diff --git a/src/main/java/org/radarbase/management/web/rest/MetaTokenResource.kt b/src/main/java/org/radarbase/management/web/rest/MetaTokenResource.kt deleted file mode 100644 index 7c9a412a5..000000000 --- a/src/main/java/org/radarbase/management/web/rest/MetaTokenResource.kt +++ /dev/null @@ -1,91 +0,0 @@ -package org.radarbase.management.web.rest - -import io.micrometer.core.annotation.Timed -import org.radarbase.auth.authorization.EntityDetails -import org.radarbase.auth.authorization.Permission -import org.radarbase.management.security.Constants -import org.radarbase.management.security.NotAuthorizedException -import org.radarbase.management.service.AuthService -import org.radarbase.management.service.MetaTokenService -import org.radarbase.management.service.dto.TokenDTO -import org.radarbase.management.web.rest.OAuthClientsResource -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.DeleteMapping -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController -import java.net.MalformedURLException -import java.time.Duration - -//@RestController -//@RequestMapping("/api") -class MetaTokenResource { - @Autowired - private val metaTokenService: MetaTokenService? = null - - @Autowired - private val authService: AuthService? = null - - /** - * GET /api/meta-token/:tokenName. - * - * - * Get refresh-token available under this tokenName. - * - * @param tokenName the tokenName given after pairing the subject with client - * @return the client as a [ClientPairInfoDTO] - */ - @GetMapping("/meta-token/{tokenName:" + Constants.TOKEN_NAME_REGEX + "}") - @Timed - @Throws( - MalformedURLException::class - ) - fun getTokenByTokenName(@PathVariable("tokenName") tokenName: String?): ResponseEntity { - log.info("Requesting token with tokenName {}", tokenName) - return ResponseEntity.ok().body(tokenName?.let { metaTokenService!!.fetchToken(it) }) - } - - /** - * DELETE /api/meta-token/:tokenName. - * - * - * Delete refresh-token available under this tokenName. - * - * @param tokenName the tokenName given after pairing the subject with client - * @return the client as a [ClientPairInfoDTO] - */ - @DeleteMapping("/meta-token/{tokenName:" + Constants.TOKEN_NAME_REGEX + "}") - @Timed - @Throws( - NotAuthorizedException::class - ) - fun deleteTokenByTokenName(@PathVariable("tokenName") tokenName: String?): ResponseEntity { - log.info("Requesting token with tokenName {}", tokenName) - val metaToken = tokenName?.let { metaTokenService!!.getToken(it) } - val subject = metaToken?.subject - val project: String = subject!! - .activeProject - ?.projectName - ?: - throw NotAuthorizedException( - "Cannot establish authority of subject without active project affiliation." - ) - val user = subject.user!!.login - authService!!.checkPermission( - Permission.SUBJECT_UPDATE, - { e: EntityDetails -> e.project(project).subject(user) }) - metaTokenService?.delete(metaToken) - return ResponseEntity.noContent().build() - } - - companion object { - private val log = LoggerFactory.getLogger(OAuthClientsResource::class.java) - @JvmField - val DEFAULT_META_TOKEN_TIMEOUT = Duration.ofHours(1) - @JvmField - val DEFAULT_PERSISTENT_META_TOKEN_TIMEOUT = Duration.ofDays(31) - } -} diff --git a/src/main/java/org/radarbase/management/web/rest/OAuthClientsResource.kt b/src/main/java/org/radarbase/management/web/rest/OAuthClientsResource.kt deleted file mode 100644 index 68e9978d7..000000000 --- a/src/main/java/org/radarbase/management/web/rest/OAuthClientsResource.kt +++ /dev/null @@ -1,227 +0,0 @@ -package org.radarbase.management.web.rest - -import io.micrometer.core.annotation.Timed -import org.radarbase.auth.authorization.EntityDetails -import org.radarbase.auth.authorization.Permission -import org.radarbase.management.security.Constants -import org.radarbase.management.security.NotAuthorizedException -import org.radarbase.management.service.AuthService -import org.radarbase.management.service.MetaTokenService -import org.radarbase.management.service.OAuthClientService -import org.radarbase.management.service.ResourceUriService -import org.radarbase.management.service.SubjectService -import org.radarbase.management.service.UserService -import org.radarbase.management.service.dto.ClientDetailsDTO -import org.radarbase.management.service.dto.ClientPairInfoDTO -import org.radarbase.management.service.mapper.ClientDetailsMapper -import org.radarbase.management.web.rest.errors.EntityName -import org.radarbase.management.web.rest.errors.ErrorConstants -import org.radarbase.management.web.rest.errors.NotFoundException -import org.radarbase.management.web.rest.util.HeaderUtil -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.actuate.audit.AuditEvent -import org.springframework.boot.actuate.audit.AuditEventRepository -import org.springframework.http.HttpStatus -import org.springframework.http.ResponseEntity -import org.springframework.security.access.AccessDeniedException -import org.springframework.web.bind.annotation.DeleteMapping -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.PutMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.RestController -import java.net.MalformedURLException -import java.net.URISyntaxException -import javax.validation.Valid - -/** - * Created by dverbeec on 5/09/2017. - */ -//@RestController -//@RequestMapping("/api") -class OAuthClientsResource( - @Autowired private val oAuthClientService: OAuthClientService, - @Autowired private val metaTokenService: MetaTokenService, - @Autowired private val clientDetailsMapper: ClientDetailsMapper, - @Autowired private val subjectService: SubjectService, - @Autowired private val userService: UserService, - @Autowired private val eventRepository: AuditEventRepository, - @Autowired private val authService: AuthService -) { - - @Throws(NotAuthorizedException::class) - @Timed - @GetMapping("/oauth-clients") - /** - * GET /api/oauth-clients. - * - * - * Retrieve a list of currently registered OAuth clients. - * - * @return the list of registered clients as a list of [ClientDetailsDTO] - */ - fun oAuthClients(): ResponseEntity> { - authService.checkScope(Permission.OAUTHCLIENTS_READ) - val clients = clientDetailsMapper.clientDetailsToClientDetailsDTO(oAuthClientService.findAllOAuthClients()) - return ResponseEntity.ok().body(clients) - } - - /** - * GET /api/oauth-clients/:id. - * - * - * Get details on a specific client. - * - * @param id the client id for which to fetch the details - * @return the client as a [ClientDetailsDTO] - */ - @GetMapping("/oauth-clients/{id:" + Constants.ENTITY_ID_REGEX + "}") - @Timed - @Throws( - NotAuthorizedException::class - ) - fun getOAuthClientById(@PathVariable("id") id: String?): ResponseEntity { - authService.checkPermission(Permission.OAUTHCLIENTS_READ) - - val client = oAuthClientService.findOneByClientId(id) - val clientDTO = clientDetailsMapper.clientDetailsToClientDetailsDTO(client) - - // getOAuthClient checks if the id exists - return ResponseEntity.ok().body(clientDTO) - } - - /** - * PUT /api/oauth-clients. - * - * - * Update an existing OAuth client. - * - * @param clientDetailsDto The client details to update - * @return The updated OAuth client. - */ - @PutMapping("/oauth-clients") - @Timed - @Throws(NotAuthorizedException::class) - fun updateOAuthClient(@RequestBody @Valid clientDetailsDto: ClientDetailsDTO?): ResponseEntity { - authService.checkPermission(Permission.OAUTHCLIENTS_UPDATE) - // getOAuthClient checks if the id exists - OAuthClientService.checkProtected(oAuthClientService.findOneByClientId(clientDetailsDto!!.clientId)) - val updated = oAuthClientService.updateOauthClient(clientDetailsDto) - return ResponseEntity.ok() - .headers( - HeaderUtil.createEntityUpdateAlert( - EntityName.OAUTH_CLIENT, - clientDetailsDto.clientId - ) - ) - .body(clientDetailsMapper.clientDetailsToClientDetailsDTO(updated)) - } - - /** - * DELETE /api/oauth-clients/:id. - * - * - * Delete the OAuth client with the specified client id. - * - * @param id The id of the client to delete - * @return a ResponseEntity indicating success or failure - */ - @DeleteMapping("/oauth-clients/{id:" + Constants.ENTITY_ID_REGEX + "}") - @Timed - @Throws( - NotAuthorizedException::class - ) - fun deleteOAuthClient(@PathVariable id: String?): ResponseEntity { - authService.checkPermission(Permission.OAUTHCLIENTS_DELETE) - // getOAuthClient checks if the id exists - OAuthClientService.checkProtected(oAuthClientService.findOneByClientId(id)) - oAuthClientService.deleteClientDetails(id) - return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(EntityName.OAUTH_CLIENT, id)) - .build() - } - - /** - * POST /api/oauth-clients. - * - * - * Register a new oauth client. - * - * @param clientDetailsDto The OAuth client to be registered - * @return a response indicating success or failure - * @throws URISyntaxException if there was a problem formatting the URI to the new entity - */ - @PostMapping("/oauth-clients") - @Timed - @Throws(URISyntaxException::class, NotAuthorizedException::class) - fun createOAuthClient(@RequestBody clientDetailsDto: @Valid ClientDetailsDTO): ResponseEntity { - authService.checkPermission(Permission.OAUTHCLIENTS_CREATE) - val created = oAuthClientService.createClientDetail(clientDetailsDto) - return ResponseEntity.created(ResourceUriService.getUri(clientDetailsDto)) - .headers(HeaderUtil.createEntityCreationAlert(EntityName.OAUTH_CLIENT, created.clientId)) - .body(clientDetailsMapper.clientDetailsToClientDetailsDTO(created)) - } - - /** - * GET /oauth-clients/pair. - * - * - * Generates OAuth2 refresh tokens for the given user, to be used to bootstrap the - * authentication of client apps. This will generate a refresh token which can be used at the - * /oauth/token endpoint to get a new access token and refresh token. - * - * @param login the login of the subject for whom to generate pairing information - * @param clientId the OAuth client id - * @return a ClientPairInfoDTO with status 200 (OK) - */ - @GetMapping("/oauth-clients/pair") - @Timed - @Throws(NotAuthorizedException::class, URISyntaxException::class, MalformedURLException::class) - fun getRefreshToken( - @RequestParam login: String, - @RequestParam(value = "clientId") clientId: String, - @RequestParam(value = "persistent", defaultValue = "false") persistent: Boolean? - ): ResponseEntity { - authService.checkScope(Permission.SUBJECT_UPDATE) - val currentUser = - userService.getUserWithAuthorities() // We only allow this for actual logged in users for now, not for client_credentials - ?: throw AccessDeniedException( - "You must be a logged in user to access this resource" - ) - - // lookup the subject - val subject = subjectService.findOneByLogin(login) - val projectName: String = subject.activeProject - ?.projectName - ?: throw NotFoundException( - "Project for subject $login not found", EntityName.SUBJECT, - ErrorConstants.ERR_SUBJECT_NOT_FOUND - ) - - - // Users who can update a subject can also generate a refresh token for that subject - authService.checkPermission( - Permission.SUBJECT_UPDATE, - { e: EntityDetails -> e.project(projectName).subject(login) }) - val cpi = metaTokenService.createMetaToken(subject, clientId, persistent!!) - // generate audit event - eventRepository.add( - AuditEvent( - currentUser.login, "PAIR_CLIENT_REQUEST", - "client_id=$clientId", "subject_login=$login" - ) - ) - log.info( - "[{}] by {}: client_id={}, subject_login={}", "PAIR_CLIENT_REQUEST", currentUser - .login, clientId, login - ) - return ResponseEntity(cpi, HttpStatus.OK) - } - - companion object { - private val log = LoggerFactory.getLogger(OAuthClientsResource::class.java) - } -} diff --git a/src/test/java/org/radarbase/auth/authentication/OAuthHelper.kt b/src/test/java/org/radarbase/auth/authentication/OAuthHelper.kt index 5aa0cd8e7..cbf2b75b2 100644 --- a/src/test/java/org/radarbase/auth/authentication/OAuthHelper.kt +++ b/src/test/java/org/radarbase/auth/authentication/OAuthHelper.kt @@ -11,7 +11,6 @@ import org.radarbase.management.domain.Role import org.radarbase.management.domain.User import org.radarbase.management.repository.UserRepository import org.radarbase.management.security.JwtAuthenticationFilter -import org.radarbase.management.security.jwt.ManagementPortalJwtAccessTokenConverter import org.slf4j.LoggerFactory import org.springframework.mock.web.MockHttpServletRequest import org.springframework.security.core.Authentication @@ -38,7 +37,7 @@ object OAuthHelper { val AUTHORITIES = arrayOf("ROLE_SYS_ADMIN") val ROLES = arrayOf("ROLE_SYS_ADMIN") val SOURCES = arrayOf() - val AUD = arrayOf(ManagementPortalJwtAccessTokenConverter.RES_MANAGEMENT_PORTAL) + val AUD = arrayOf("res_ManagementPortal") const val CLIENT = "unit_test" const val USER = "admin" const val ISS = "RADAR" @@ -110,7 +109,7 @@ object OAuthHelper { validRsaToken = createValidToken(rsa) val verifierList = listOf(ecdsa, rsa) .map { alg: Algorithm? -> - alg?.toTokenVerifier(ManagementPortalJwtAccessTokenConverter.RES_MANAGEMENT_PORTAL) + alg?.toTokenVerifier("res_ManagementPortal") } .requireNoNulls() .toList() @@ -164,7 +163,6 @@ object OAuthHelper { .withArrayClaim("authorities", AUTHORITIES) .withArrayClaim("roles", ROLES) .withArrayClaim("sources", SOURCES) - .withArrayClaim("aud", AUD) .withClaim("client_id", CLIENT) .withClaim("user_name", USER) .withClaim("jti", JTI) diff --git a/src/test/java/org/radarbase/management/service/MetaTokenServiceTest.kt b/src/test/java/org/radarbase/management/service/MetaTokenServiceTest.kt deleted file mode 100644 index 55834309e..000000000 --- a/src/test/java/org/radarbase/management/service/MetaTokenServiceTest.kt +++ /dev/null @@ -1,136 +0,0 @@ -package org.radarbase.management.service - -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.radarbase.management.ManagementPortalTestApp -import org.radarbase.management.domain.MetaToken -import org.radarbase.management.repository.MetaTokenRepository -import org.radarbase.management.service.dto.SubjectDTO -import org.radarbase.management.service.mapper.SubjectMapper -import org.radarbase.management.web.rest.errors.RadarWebApplicationException -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.security.oauth2.provider.ClientDetails -import org.springframework.test.context.junit.jupiter.SpringExtension -import org.springframework.transaction.annotation.Transactional -import java.net.MalformedURLException -import java.time.Duration -import java.time.Instant -import java.util.* - -/** - * Test class for the MetaTokenService class. - * - * @see MetaTokenService - */ -@ExtendWith(SpringExtension::class) -@SpringBootTest(classes = [ManagementPortalTestApp::class]) -@Transactional -internal class MetaTokenServiceTest( - @Autowired private val metaTokenService: MetaTokenService, - @Autowired private val metaTokenRepository: MetaTokenRepository, - @Autowired private val subjectService: SubjectService, - @Autowired private val subjectMapper: SubjectMapper, - @Autowired private val oAuthClientService: OAuthClientService, -) { - private lateinit var clientDetails: ClientDetails - private lateinit var subjectDto: SubjectDTO - - @BeforeEach - fun setUp() { - subjectDto = SubjectServiceTest.createEntityDTO() - subjectDto = subjectService.createSubject(subjectDto)!! - clientDetails = oAuthClientService.createClientDetail(OAuthClientServiceTestUtil.createClient()) - } - - @Test - @Throws(MalformedURLException::class) - fun testSaveThenFetchMetaToken() { - val metaToken = MetaToken() - .generateName(MetaToken.SHORT_ID_LENGTH) - .fetched(false) - .persistent(false) - .expiryDate(Instant.now().plus(Duration.ofHours(1))) - .subject(subjectMapper.subjectDTOToSubject(subjectDto)) - .clientId(clientDetails.clientId) - val saved = metaTokenService.save(metaToken) - Assertions.assertNotNull(saved.id) - Assertions.assertNotNull(saved.tokenName) - Assertions.assertFalse(saved.isFetched()) - Assertions.assertTrue(saved.expiryDate!!.isAfter(Instant.now())) - val tokenName = saved.tokenName - val fetchedToken = metaTokenService.fetchToken(tokenName!!) - Assertions.assertNotNull(fetchedToken) - Assertions.assertNotNull(fetchedToken.refreshToken) - } - - @Test - @Throws(MalformedURLException::class) - fun testGetAFetchedMetaToken() { - val token = MetaToken() - .generateName(MetaToken.SHORT_ID_LENGTH) - .fetched(true) - .persistent(false) - .tokenName("something") - .expiryDate(Instant.now().plus(Duration.ofHours(1))) - .subject(subjectMapper.subjectDTOToSubject(subjectDto)) - val saved = metaTokenService.save(token) - Assertions.assertNotNull(saved.id) - Assertions.assertNotNull(saved.tokenName) - Assertions.assertTrue(saved.isFetched()) - Assertions.assertTrue(saved.expiryDate!!.isAfter(Instant.now())) - val tokenName = saved.tokenName - Assertions.assertThrows( - RadarWebApplicationException::class.java - ) { metaTokenService.fetchToken(tokenName!!) } - } - - @Test - @Throws(MalformedURLException::class) - fun testGetAnExpiredMetaToken() { - val token = MetaToken() - .generateName(MetaToken.SHORT_ID_LENGTH) - .fetched(false) - .persistent(false) - .tokenName("somethingelse") - .expiryDate(Instant.now().minus(Duration.ofHours(1))) - .subject(subjectMapper.subjectDTOToSubject(subjectDto)) - val saved = metaTokenService.save(token) - Assertions.assertNotNull(saved.id) - Assertions.assertNotNull(saved.tokenName) - Assertions.assertFalse(saved.isFetched()) - Assertions.assertTrue(saved.expiryDate!!.isBefore(Instant.now())) - val tokenName = saved.tokenName - Assertions.assertThrows( - RadarWebApplicationException::class.java - ) { metaTokenService.fetchToken(tokenName!!) } - } - - @Test - fun testRemovingExpiredMetaToken() { - val tokenFetched = MetaToken() - .generateName(MetaToken.SHORT_ID_LENGTH) - .fetched(true) - .persistent(false) - .tokenName("something") - .expiryDate(Instant.now().plus(Duration.ofHours(1))) - val tokenExpired = MetaToken() - .generateName(MetaToken.SHORT_ID_LENGTH) - .fetched(false) - .persistent(false) - .tokenName("somethingelse") - .expiryDate(Instant.now().minus(Duration.ofHours(1))) - val tokenNew = MetaToken() - .generateName(MetaToken.SHORT_ID_LENGTH) - .fetched(false) - .persistent(false) - .tokenName("somethingelseandelse") - .expiryDate(Instant.now().plus(Duration.ofHours(1))) - metaTokenRepository.saveAll(Arrays.asList(tokenFetched, tokenExpired, tokenNew)) - metaTokenService.removeStaleTokens() - val availableTokens = metaTokenRepository.findAll() - Assertions.assertEquals(1, availableTokens.size) - } -} diff --git a/src/test/java/org/radarbase/management/web/rest/OAuthClientsResourceIntTest.kt b/src/test/java/org/radarbase/management/web/rest/OAuthClientsResourceIntTest.kt deleted file mode 100644 index cb12ebb4c..000000000 --- a/src/test/java/org/radarbase/management/web/rest/OAuthClientsResourceIntTest.kt +++ /dev/null @@ -1,212 +0,0 @@ -package org.radarbase.management.web.rest - -import org.assertj.core.api.Assertions -import org.hamcrest.Matchers -import org.hamcrest.Matchers.containsInAnyOrder -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.mockito.MockitoAnnotations -import org.radarbase.auth.authentication.OAuthHelper -import org.radarbase.management.ManagementPortalApp -import org.radarbase.management.service.OAuthClientServiceTestUtil -import org.radarbase.management.service.dto.ClientDetailsDTO -import org.radarbase.management.web.rest.errors.ExceptionTranslator -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.data.web.PageableHandlerMethodArgumentResolver -import org.springframework.http.MediaType -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter -import org.springframework.mock.web.MockFilterConfig -import org.springframework.security.core.GrantedAuthority -import org.springframework.security.oauth2.provider.ClientDetails -import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService -import org.springframework.test.context.junit.jupiter.SpringExtension -import org.springframework.test.web.servlet.MockMvc -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders -import org.springframework.test.web.servlet.result.MockMvcResultMatchers -import org.springframework.test.web.servlet.setup.MockMvcBuilders -import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder -import org.springframework.transaction.annotation.Transactional -import java.util.function.Consumer - -/** - * Test class for the ProjectResource REST controller. - * - * @see ProjectResource - */ -@ExtendWith(SpringExtension::class) -@SpringBootTest(classes = [ManagementPortalApp::class]) -internal class OAuthClientsResourceIntTest @Autowired constructor( - @Autowired private val oauthClientsResource: OAuthClientsResource, - @Autowired private val clientDetailsService: JdbcClientDetailsService, - @Autowired private val jacksonMessageConverter: MappingJackson2HttpMessageConverter, - @Autowired private val pageableArgumentResolver: PageableHandlerMethodArgumentResolver, - @Autowired private val exceptionTranslator: ExceptionTranslator, -) { - private lateinit var restOauthClientMvc: MockMvc - private lateinit var details: ClientDetailsDTO - private var databaseSizeBeforeCreate: Int = 0 - private lateinit var clientDetailsList: List - - @BeforeEach - @Throws(Exception::class) - fun setUp() { - MockitoAnnotations.openMocks(this) - val filter = OAuthHelper.createAuthenticationFilter() - filter.init(MockFilterConfig()) - restOauthClientMvc = - MockMvcBuilders.standaloneSetup(oauthClientsResource).setCustomArgumentResolvers(pageableArgumentResolver) - .setControllerAdvice(exceptionTranslator).setMessageConverters(jacksonMessageConverter) - .addFilter(filter).defaultRequest( - MockMvcRequestBuilders.get("/").with(OAuthHelper.bearerToken()) - ).build() - databaseSizeBeforeCreate = clientDetailsService.listClientDetails().size - - // Create the OAuth Client - details = OAuthClientServiceTestUtil.createClient() - restOauthClientMvc.perform( - MockMvcRequestBuilders.post("/api/oauth-clients").contentType(TestUtil.APPLICATION_JSON_UTF8) - .content(TestUtil.convertObjectToJsonBytes(details)) - ).andExpect(MockMvcResultMatchers.status().isCreated()) - - // Validate the Project in the database - clientDetailsList = clientDetailsService.listClientDetails() - Assertions.assertThat(clientDetailsList).hasSize(databaseSizeBeforeCreate + 1) - } - - @Test - @Transactional - @Throws(Exception::class) - fun createAndFetchOAuthClient() { - // fetch the created oauth client and check the json result - restOauthClientMvc.perform( - MockMvcRequestBuilders.get("/api/oauth-clients/" + details.clientId).accept(MediaType.APPLICATION_JSON) - ).andExpect( - MockMvcResultMatchers.status().isOk() - ).andExpect( - MockMvcResultMatchers.jsonPath("$.clientId").value(Matchers.equalTo(details.clientId)) - ).andExpect(MockMvcResultMatchers.jsonPath("$.clientSecret").value(Matchers.nullValue())).andExpect( - MockMvcResultMatchers.jsonPath("$.accessTokenValiditySeconds").value( - Matchers.equalTo( - details.accessTokenValiditySeconds?.toInt() - ) - ) - ).andExpect( - MockMvcResultMatchers.jsonPath("$.refreshTokenValiditySeconds").value( - Matchers.equalTo( - details.refreshTokenValiditySeconds?.toInt() - ) - ) - ).andExpect( - MockMvcResultMatchers.jsonPath("$.scope") - .value(containsInAnyOrder(details.scope?.map { Matchers.equalTo(it) })) - ).andExpect(MockMvcResultMatchers.jsonPath("$.autoApproveScopes") - .value(containsInAnyOrder(details.autoApproveScopes?.map { Matchers.equalTo(it) }))) - .andExpect(MockMvcResultMatchers.jsonPath("$.authorizedGrantTypes") - .value(containsInAnyOrder(details.authorizedGrantTypes?.map { Matchers.equalTo(it) }))).andExpect( - MockMvcResultMatchers.jsonPath("$.authorities").value( - containsInAnyOrder(details.authorities?.map { Matchers.equalTo(it) }) - ) - ) - - val testDetails = - clientDetailsList.stream().filter { d: ClientDetails -> d.clientId == details.clientId }.findFirst() - .orElseThrow() - Assertions.assertThat(testDetails.clientSecret).startsWith("$2a$10$") - Assertions.assertThat(testDetails.scope).containsExactlyInAnyOrderElementsOf( - details.scope - ) - Assertions.assertThat(testDetails.resourceIds).containsExactlyInAnyOrderElementsOf( - details.resourceIds - ) - Assertions.assertThat(testDetails.authorizedGrantTypes).containsExactlyInAnyOrderElementsOf( - details.authorizedGrantTypes - ) - details.autoApproveScopes?.forEach(Consumer { scope: String? -> - Assertions.assertThat( - testDetails.isAutoApprove( - scope - ) - ).isTrue() - }) - Assertions.assertThat(testDetails.accessTokenValiditySeconds).isEqualTo( - details.accessTokenValiditySeconds?.toInt() - ) - Assertions.assertThat(testDetails.refreshTokenValiditySeconds).isEqualTo( - details.refreshTokenValiditySeconds?.toInt() - ) - Assertions.assertThat(testDetails.authorities.stream().map { obj: GrantedAuthority -> obj.authority }) - .containsExactlyInAnyOrderElementsOf(details.authorities) - Assertions.assertThat(testDetails.additionalInformation).containsAllEntriesOf( - details.additionalInformation - ) - } - - @Test - @Transactional - @Throws(Exception::class) - fun duplicateOAuthClient() { - restOauthClientMvc.perform( - MockMvcRequestBuilders.post("/api/oauth-clients").contentType(TestUtil.APPLICATION_JSON_UTF8) - .content(TestUtil.convertObjectToJsonBytes(details)) - ).andExpect(MockMvcResultMatchers.status().isConflict()) - } - - @Test - @Transactional - @Throws(Exception::class) - fun updateOAuthClient() { - // update the client - details.refreshTokenValiditySeconds = 20L - restOauthClientMvc.perform( - MockMvcRequestBuilders.put("/api/oauth-clients").contentType(TestUtil.APPLICATION_JSON_UTF8) - .content(TestUtil.convertObjectToJsonBytes(details)) - ).andExpect(MockMvcResultMatchers.status().isOk()) - - // fetch the client - clientDetailsList = clientDetailsService.listClientDetails() - Assertions.assertThat(clientDetailsList).hasSize(databaseSizeBeforeCreate + 1) - val testDetails = - clientDetailsList.stream().filter { d: ClientDetails -> d.clientId == details.clientId }.findFirst() - .orElseThrow() - Assertions.assertThat(testDetails.refreshTokenValiditySeconds).isEqualTo(20) - } - - @Test - @Transactional - @Throws(Exception::class) - fun deleteOAuthClient() { - restOauthClientMvc.perform( - MockMvcRequestBuilders.delete("/api/oauth-clients/" + details.clientId) - .contentType(TestUtil.APPLICATION_JSON_UTF8).content(TestUtil.convertObjectToJsonBytes(details)) - ).andExpect(MockMvcResultMatchers.status().isOk()) - val clientDetailsList = clientDetailsService.listClientDetails() - Assertions.assertThat(clientDetailsList.size).isEqualTo(databaseSizeBeforeCreate) - } - - @Test - @Transactional - @Throws(Exception::class) - fun cannotModifyProtected() { - // first change our test client to be protected - details.additionalInformation!!["protected"] = "true" - restOauthClientMvc.perform( - MockMvcRequestBuilders.put("/api/oauth-clients").contentType(TestUtil.APPLICATION_JSON_UTF8) - .content(TestUtil.convertObjectToJsonBytes(details)) - ).andExpect(MockMvcResultMatchers.status().isOk()) - - // expect we can not delete it now - restOauthClientMvc.perform( - MockMvcRequestBuilders.delete("/api/oauth-clients/" + details.clientId) - .contentType(TestUtil.APPLICATION_JSON_UTF8).content(TestUtil.convertObjectToJsonBytes(details)) - ).andExpect(MockMvcResultMatchers.status().isForbidden()) - - // expect we can not update it now - details.refreshTokenValiditySeconds = 20L - restOauthClientMvc.perform( - MockMvcRequestBuilders.put("/api/oauth-clients").contentType(TestUtil.APPLICATION_JSON_UTF8) - .content(TestUtil.convertObjectToJsonBytes(details)) - ).andExpect(MockMvcResultMatchers.status().isForbidden()) - } -} From 5e6f20dde0621852fc36f0ea7468bc014023c319 Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 23 Aug 2024 00:00:50 +0100 Subject: [PATCH 16/22] Restore ory stack changes --- src/main/docker/ory_stack.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docker/ory_stack.yml b/src/main/docker/ory_stack.yml index eb94e4391..acac23a8c 100644 --- a/src/main/docker/ory_stack.yml +++ b/src/main/docker/ory_stack.yml @@ -10,7 +10,7 @@ services: - ORY_SDK_URL=http://kratos:4433/ - HYDRA_ADMIN_URL=http://hydra:4445 ports: - - "3002:4455" + - "3000:3000" volumes: - /tmp/ui-node/logs:/root/.npm/_logs From eaa7d33545ebf919004556b56a8eda7f85caa838 Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 23 Aug 2024 12:54:47 +0100 Subject: [PATCH 17/22] Update JwtAuthenticationFilter issues: make sure existing auth is checked and security context is cleared after --- .../config/SecurityConfiguration.kt | 5 +- .../security/JwtAuthenticationFilter.kt | 174 +++++++++--------- 2 files changed, 86 insertions(+), 93 deletions(-) diff --git a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt index 446f691b4..0b1c33885 100644 --- a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt +++ b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt @@ -76,6 +76,9 @@ class SecurityConfiguration @Bean fun jwtAuthenticationFilter(): JwtAuthenticationFilter = JwtAuthenticationFilter(tokenValidator, authenticationManager()) + .skipUrlPattern(HttpMethod.GET, "/") + .skipUrlPattern(HttpMethod.GET, "/*.{js,ico,css,html}") + .skipUrlPattern(HttpMethod.GET, "/i18n/**") .skipUrlPattern(HttpMethod.GET, "/management/health") .skipUrlPattern(HttpMethod.POST, "/oauth/token") .skipUrlPattern(HttpMethod.GET, "/api/meta-token/*") @@ -103,10 +106,10 @@ class SecurityConfiguration .antMatchers("/api-docs/**") .antMatchers("/swagger-ui.html") .antMatchers("/api-docs{,.json,.yml}") - .antMatchers("/api/login") .antMatchers("/api/logout-url") .antMatchers("/api/profile-info") .antMatchers("/api/activate") + .antMatchers("/api/sitesettings") .antMatchers("/api/redirect/**") .antMatchers("/api/account/reset_password/init") .antMatchers("/api/account/reset_password/finish") diff --git a/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt index e665cfed1..d2c4de919 100644 --- a/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt +++ b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt @@ -31,8 +31,8 @@ class JwtAuthenticationFilter( private val ignoreUrls: MutableList = mutableListOf() fun skipUrlPattern(method: HttpMethod, vararg antPatterns: String?): JwtAuthenticationFilter { - antPatterns.forEach { pattern -> - pattern?.let { ignoreUrls.add(AntPathRequestMatcher(it, method.name)) } + for (pattern in antPatterns) { + ignoreUrls.add(AntPathRequestMatcher(pattern, method.name)) } return this } @@ -41,131 +41,121 @@ class JwtAuthenticationFilter( override fun doFilterInternal( httpRequest: HttpServletRequest, httpResponse: HttpServletResponse, - chain: FilterChain, + chain: FilterChain ) { - logger.debug("Processing request: ${httpRequest.requestURI}") - - if (CorsUtils.isPreFlightRequest(httpRequest)) { - logger.debug("Skipping JWT check for preflight request") - chain.doFilter(httpRequest, httpResponse) - return - } - - val existingAuthentication = SecurityContextHolder.getContext().authentication - val stringToken = tokenFromHeader(httpRequest) - var token: RadarToken? = null - - if (stringToken != null) { - token = validateTokenFromHeader(stringToken, httpRequest) - } + try { + if (CorsUtils.isPreFlightRequest(httpRequest)) { + logger.debug("Skipping JWT check for ${httpRequest.requestURI}") + chain.doFilter(httpRequest, httpResponse) + return + } + val stringToken = tokenFromHeader(httpRequest) + var token: RadarToken? = null + var exMessage = "No token provided" + + if (stringToken != null) { + try { + logger.warn("Validating token from header: $stringToken") + token = validator.validateBlocking(stringToken) + val authentication = createAuthenticationFromToken(token) + SecurityContextHolder.getContext().authentication = authentication + logger.debug("JWT authentication successful") + } catch (ex: TokenValidationException) { + exMessage = ex.message ?: exMessage + logger.info("Failed to validate token from header: $exMessage") + } + } - if (token == null && existingAuthentication.isAnonymous) { - token = validateTokenFromSession(httpRequest.session) - } + if (token == null) { + val existingAuthentication = SecurityContextHolder.getContext().authentication + if (existingAuthentication != null && + existingAuthentication.isAuthenticated && + !existingAuthentication.isAnonymous + ) { + logger.info("Existing authentication found: ${existingAuthentication}") + chain.doFilter(httpRequest, httpResponse) + return + } + + val session = httpRequest.getSession(false) + token = session?.radarToken?.takeIf { Instant.now() < it.expiresAt } + if (token != null) { + logger.debug("Using token from session") + val authentication = createAuthenticationFromToken(token) + SecurityContextHolder.getContext().authentication = authentication + } + } - if (!validateAndSetAuthentication(token, httpRequest, httpResponse)) { - return + if (!validateToken(token, httpRequest, httpResponse)) { + return + } + chain.doFilter(httpRequest, httpResponse) + } finally { + SecurityContextHolder.clearContext() } + } - chain.doFilter(httpRequest, httpResponse) + private fun createAuthenticationFromToken(token: RadarToken): Authentication { + val authentication = RadarAuthentication(token) + return authenticationManager.authenticate(authentication) } - private fun validateTokenFromHeader( - tokenString: String, - httpRequest: HttpServletRequest - ): RadarToken? { - return try { - logger.debug("Validating token from header: ${tokenString}") - val token = validator.validateBlocking(tokenString) - val authentication = createAuthenticationFromToken(token) - SecurityContextHolder.getContext().authentication = authentication - logger.debug("JWT authentication successful") - token - } catch (ex: TokenValidationException) { - logger.warn("Token validation failed: ${ex.message}") - null + override fun shouldNotFilter(@Nonnull httpRequest: HttpServletRequest): Boolean { + val shouldNotFilterUrl = ignoreUrls.find { it.matches(httpRequest) } + return if (shouldNotFilterUrl != null) { + logger.debug("Skipping JWT check for ${httpRequest.requestURI}") + true + } else { + false } } - private fun validateTokenFromSession(session: HttpSession?): RadarToken? { - val token = session?.radarToken?.takeIf { Instant.now() < it.expiresAt } - if (token != null) { - logger.debug("Using token from session") - val authentication = createAuthenticationFromToken(token) - SecurityContextHolder.getContext().authentication = authentication - } - return token + private fun tokenFromHeader(httpRequest: HttpServletRequest): String? { + return httpRequest + .getHeader(HttpHeaders.AUTHORIZATION) + ?.takeIf { it.startsWith(AUTHORIZATION_BEARER_HEADER) } + ?.removePrefix(AUTHORIZATION_BEARER_HEADER) + ?.trim { it <= ' ' } } - private fun validateAndSetAuthentication( + private fun validateToken( token: RadarToken?, httpRequest: HttpServletRequest, - httpResponse: HttpServletResponse + httpResponse: HttpServletResponse, ): Boolean { return if (token != null) { httpRequest.radarToken = token - val authentication = createAuthenticationFromToken(token) + val authentication = RadarAuthentication(token) + authenticationManager.authenticate(authentication) SecurityContextHolder.getContext().authentication = authentication true + } else if (isOptional) { + logger.debug("Skipping optional token check for ${httpRequest.requestURI}") + true } else { - handleUnauthorized(httpRequest, httpResponse, "No valid token provided") + logger.error("Unauthorized - no valid token provided for ${httpRequest.requestURI}") + httpResponse.returnUnauthorized(httpRequest) false } } - private fun handleUnauthorized( - httpRequest: HttpServletRequest, - httpResponse: HttpServletResponse, - message: String - ) { - if (!isOptional) { - logger.error("Unauthorized - ${message}") - httpResponse.returnUnauthorized(httpRequest, message) - } - } - - private fun createAuthenticationFromToken(token: RadarToken): Authentication { - val authentication = RadarAuthentication(token) - return authenticationManager.authenticate(authentication) - } - - override fun shouldNotFilter(@Nonnull httpRequest: HttpServletRequest): Boolean { - return ignoreUrls.any { it.matches(httpRequest) }.also { shouldSkip -> - if (shouldSkip) { - logger.debug("Skipping JWT check for ${httpRequest.requestURL}") - } - } - } - - private fun tokenFromHeader(httpRequest: HttpServletRequest): String? { - return httpRequest - .getHeader(HttpHeaders.AUTHORIZATION) - ?.takeIf { it.startsWith(AUTHORIZATION_BEARER_HEADER) } - ?.removePrefix(AUTHORIZATION_BEARER_HEADER) - ?.trim() - } - companion object { - private val logger = LoggerFactory.getLogger(JwtAuthenticationFilter::class.java) - private const val AUTHORIZATION_BEARER_HEADER = "Bearer" - private const val TOKEN_ATTRIBUTE = "jwt" - - private fun HttpServletResponse.returnUnauthorized( - request: HttpServletRequest, - message: String? - ) { + private fun HttpServletResponse.returnUnauthorized(request: HttpServletRequest) { status = HttpServletResponse.SC_UNAUTHORIZED setHeader(HttpHeaders.WWW_AUTHENTICATE, AUTHORIZATION_BEARER_HEADER) outputStream.print( """ {"error": "Unauthorized", "status": "${HttpServletResponse.SC_UNAUTHORIZED}", - "message": "${message ?: "null"}", "path": "${request.requestURI}"} - """.trimIndent() + """.trimIndent() ) } + private val logger = LoggerFactory.getLogger(JwtAuthenticationFilter::class.java) + private const val AUTHORIZATION_BEARER_HEADER = "Bearer" + private const val TOKEN_ATTRIBUTE = "jwt" + var HttpSession.radarToken: RadarToken? get() = getAttribute(TOKEN_ATTRIBUTE) as RadarToken? set(value) = setAttribute(TOKEN_ATTRIBUTE, value) From 960a3b55bfcd0473a9860cebf4177780f01d9015 Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 23 Aug 2024 12:59:15 +0100 Subject: [PATCH 18/22] Restore deleted annotations --- .../radarbase/management/security/JwtAuthenticationFilter.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt index d2c4de919..811c712b1 100644 --- a/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt +++ b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt @@ -156,10 +156,14 @@ class JwtAuthenticationFilter( private const val AUTHORIZATION_BEARER_HEADER = "Bearer" private const val TOKEN_ATTRIBUTE = "jwt" + @get:JvmStatic + @set:JvmStatic var HttpSession.radarToken: RadarToken? get() = getAttribute(TOKEN_ATTRIBUTE) as RadarToken? set(value) = setAttribute(TOKEN_ATTRIBUTE, value) + @get:JvmStatic + @set:JvmStatic var HttpServletRequest.radarToken: RadarToken? get() = getAttribute(TOKEN_ATTRIBUTE) as RadarToken? set(value) = setAttribute(TOKEN_ATTRIBUTE, value) From c11734eb0cc26a96ad90dd21c81354d36b76399d Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 23 Aug 2024 13:30:43 +0100 Subject: [PATCH 19/22] Move access token fetching to AuthService --- .../management/service/AuthService.kt | 113 ++++++++++++---- .../management/web/rest/LoginEndpoint.kt | 121 +++++------------- 2 files changed, 121 insertions(+), 113 deletions(-) diff --git a/src/main/java/org/radarbase/management/service/AuthService.kt b/src/main/java/org/radarbase/management/service/AuthService.kt index 07929fc7e..6433af584 100644 --- a/src/main/java/org/radarbase/management/service/AuthService.kt +++ b/src/main/java/org/radarbase/management/service/AuthService.kt @@ -1,61 +1,91 @@ package org.radarbase.management.service +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import java.time.Duration +import java.util.* +import java.util.function.Consumer +import javax.annotation.Nullable import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.jsonPrimitive import org.radarbase.auth.authorization.* +import org.radarbase.auth.exception.IdpException import org.radarbase.auth.token.RadarToken +import org.radarbase.management.config.ManagementPortalProperties import org.radarbase.management.security.NotAuthorizedException +import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service -import java.util.* -import java.util.function.Consumer -import javax.annotation.Nullable @Service class AuthService( - @Nullable - private val token: RadarToken?, - private val oracle: AuthorizationOracle, + @Nullable private val token: RadarToken?, + private val oracle: AuthorizationOracle, + @Autowired private val managementPortalProperties: ManagementPortalProperties, ) { + private val httpClient = + HttpClient(CIO) { + install(HttpTimeout) { + connectTimeoutMillis = Duration.ofSeconds(10).toMillis() + socketTimeoutMillis = Duration.ofSeconds(10).toMillis() + requestTimeoutMillis = Duration.ofSeconds(300).toMillis() + } + install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } + } /** - * Check whether given [token] would have the [permission] scope in any of its roles. This doesn't - * check whether [token] has access to a specific entity or global access. + * Check whether given [token] would have the [permission] scope in any of its roles. This + * doesn't check whether [token] has access to a specific entity or global access. * @throws NotAuthorizedException if identity does not have scope */ @Throws(NotAuthorizedException::class) fun checkScope(permission: Permission) { - val token = token ?: throw NotAuthorizedException("User without authentication does not have permission.") + val token = + token + ?: throw NotAuthorizedException( + "User without authentication does not have permission." + ) if (!oracle.hasScope(token, permission)) { throw NotAuthorizedException( - "User ${token.username} with client ${token.clientId} does not have permission $permission" + "User ${token.username} with client ${token.clientId} does not have permission $permission" ) } } /** - * Check whether [token] has permission [permission], regarding given entity from [builder]. - * The permission is checked both for its - * own entity scope and for the [EntityDetails.minimumEntityOrNull] entity scope. + * Check whether [token] has permission [permission], regarding given entity from [builder]. The + * permission is checked both for its own entity scope and for the + * [EntityDetails.minimumEntityOrNull] entity scope. * @throws NotAuthorizedException if identity does not have permission */ @JvmOverloads @Throws(NotAuthorizedException::class) fun checkPermission( - permission: Permission, - builder: Consumer? = null, - scope: Permission.Entity = permission.entity, + permission: Permission, + builder: Consumer? = null, + scope: Permission.Entity = permission.entity, ) { - val token = token ?: throw NotAuthorizedException("User without authentication does not have permission.") + val token = + token + ?: throw NotAuthorizedException( + "User without authentication does not have permission." + ) // entitydetails builder is null means we require global permission val entity = if (builder != null) entityDetailsBuilder(builder) else EntityDetails.global - val hasPermission = runBlocking { - oracle.hasPermission(token, permission, entity, scope) - } + val hasPermission = runBlocking { oracle.hasPermission(token, permission, entity, scope) } if (!hasPermission) { throw NotAuthorizedException( - "User ${token.username} with client ${token.clientId} does not have permission $permission to scope " + - "$scope of $entity" + "User ${token.username} with client ${token.clientId} does not have permission $permission to scope " + + "$scope of $entity" ) } } @@ -65,11 +95,42 @@ class AuthService( return oracle.referentsByScope(token, permission) } - fun mayBeGranted(role: RoleAuthority, permission: Permission): Boolean = with(oracle) { - role.mayBeGranted(permission) - } + fun mayBeGranted(role: RoleAuthority, permission: Permission): Boolean = + with(oracle) { role.mayBeGranted(permission) } fun mayBeGranted(authorities: Collection, permission: Permission): Boolean { - return authorities.any{ mayBeGranted(it, permission) } + return authorities.any { mayBeGranted(it, permission) } + } + + suspend fun fetchAccessToken(code: String): String { + val tokenUrl = "${managementPortalProperties.authServer.serverUrl}/oauth2/token" + val response = + httpClient.post(tokenUrl) { + contentType(ContentType.Application.FormUrlEncoded) + accept(ContentType.Application.Json) + setBody( + Parameters.build { + append("grant_type", "authorization_code") + append("code", code) + append( + "redirect_uri", + "${managementPortalProperties.common.baseUrl}/api/redirect/login" + ) + append( + "client_id", + managementPortalProperties.frontend.clientId + ) + } + .formUrlEncode(), + ) + } + + if (response.status.isSuccess()) { + val responseMap = response.body>() + return responseMap["access_token"]?.jsonPrimitive?.content + ?: throw IdpException("Access token not found in response") + } else { + throw IdpException("Unable to get access token") + } } } diff --git a/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt index 202e701ae..a61b73b6d 100644 --- a/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt +++ b/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt @@ -1,107 +1,54 @@ package org.radarbase.management.web.rest -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.engine.cio.* -import io.ktor.client.plugins.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.client.request.* -import io.ktor.http.* -import io.ktor.serialization.kotlinx.json.* -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.jsonPrimitive -import org.radarbase.auth.exception.IdpException +import java.time.Instant import org.radarbase.management.config.ManagementPortalProperties -import org.slf4j.LoggerFactory +import org.radarbase.management.service.AuthService import org.springframework.beans.factory.annotation.Autowired import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import org.springframework.web.servlet.view.RedirectView -import java.time.Duration -import java.time.Instant @RestController @RequestMapping("/api") class LoginEndpoint - @Autowired - constructor( +@Autowired +constructor( private val managementPortalProperties: ManagementPortalProperties, - ) { - private val httpClient = - HttpClient(CIO) { - install(HttpTimeout) { - connectTimeoutMillis = Duration.ofSeconds(10).toMillis() - socketTimeoutMillis = Duration.ofSeconds(10).toMillis() - requestTimeoutMillis = Duration.ofSeconds(300).toMillis() - } - install(ContentNegotiation) { - json(Json { ignoreUnknownKeys = true }) - } - } + @Autowired private val authService: AuthService +) { - @GetMapping("/redirect/login") - suspend fun loginRedirect( + @GetMapping("/redirect/login") + suspend fun loginRedirect( @RequestParam(required = false) code: String?, - ): RedirectView { - val redirectView = RedirectView() - val config = managementPortalProperties - val mpUrl = config.common.baseUrl - - if (code == null) { - redirectView.url = buildAuthUrl(config, mpUrl) - } else { - val accessToken = fetchAccessToken(code, config) - redirectView.url = "$mpUrl/#/?access_token=$accessToken" - } - return redirectView - } - - @GetMapping("/redirect/account") - fun settingsRedirect(): RedirectView { - val redirectView = RedirectView() - redirectView.url = "${managementPortalProperties.identityServer.loginUrl}/settings" - return redirectView - } - - private fun buildAuthUrl(config: ManagementPortalProperties, mpUrl: String): String { - return "${config.authServer.loginUrl}/oauth2/auth?" + - "client_id=${config.frontend.clientId}&" + - "response_type=code&" + - "state=${Instant.now()}&" + - "audience=res_ManagementPortal&" + - "scope=offline&" + - "redirect_uri=$mpUrl/api/redirect/login" + ): RedirectView { + val redirectView = RedirectView() + + if (code == null) { + redirectView.url = buildAuthUrl() + } else { + val accessToken = authService.fetchAccessToken(code) + redirectView.url = + "${managementPortalProperties.common.baseUrl}/#/?access_token=$accessToken" } + return redirectView + } - private suspend fun fetchAccessToken( - code: String, - config: ManagementPortalProperties, - ): String { - val tokenUrl = "${config.authServer.serverUrl}/oauth2/token" - val response = - httpClient.post(tokenUrl) { - contentType(ContentType.Application.FormUrlEncoded) - accept(ContentType.Application.Json) - setBody( - Parameters - .build { - append("grant_type", "authorization_code") - append("code", code) - append("redirect_uri", "${config.common.baseUrl}/api/redirect/login") - append("client_id", config.frontend.clientId) - }.formUrlEncode(), - ) - } + @GetMapping("/redirect/account") + fun settingsRedirect(): RedirectView { + val redirectView = RedirectView() + redirectView.url = "${managementPortalProperties.identityServer.loginUrl}/settings" + return redirectView + } - if (response.status.isSuccess()) { - val responseMap = response.body>() - return responseMap["access_token"]?.jsonPrimitive?.content - ?: throw IdpException("Access token not found in response") - } else { - throw IdpException("Unable to get access token") - } - } + private fun buildAuthUrl(): String { + return "${managementPortalProperties.authServer.loginUrl}/oauth2/auth?" + + "client_id=${managementPortalProperties.frontend.clientId}&" + + "response_type=code&" + + "state=${Instant.now()}&" + + "audience=res_ManagementPortal&" + + "scope=offline&" + + "redirect_uri=${managementPortalProperties.common.baseUrl}/api/redirect/login" } +} From 2988114858a7cd4b05c6849af4f8ceef233b55e1 Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 23 Aug 2024 13:30:59 +0100 Subject: [PATCH 20/22] Invalidate session on logout --- .../org/radarbase/management/config/SecurityConfiguration.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt index 0b1c33885..230b39652 100644 --- a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt +++ b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt @@ -134,6 +134,9 @@ class SecurityConfiguration ) .authorizeRequests() .anyRequest().authenticated() + .and() + .logout().invalidateHttpSession(true) + .logoutUrl("/api/logout") } @Bean From 2c4683c34d9eeac9fa75460fc1719714aa4bf98a Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 23 Aug 2024 14:04:45 +0100 Subject: [PATCH 21/22] Fix error component login url and remove unnecessary logs --- .../radarbase/management/security/JwtAuthenticationFilter.kt | 1 - src/main/webapp/app/layouts/error/error.component.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt index 811c712b1..d60b5e8ab 100644 --- a/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt +++ b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt @@ -72,7 +72,6 @@ class JwtAuthenticationFilter( existingAuthentication.isAuthenticated && !existingAuthentication.isAnonymous ) { - logger.info("Existing authentication found: ${existingAuthentication}") chain.doFilter(httpRequest, httpResponse) return } diff --git a/src/main/webapp/app/layouts/error/error.component.ts b/src/main/webapp/app/layouts/error/error.component.ts index c439100c1..31ac48f78 100644 --- a/src/main/webapp/app/layouts/error/error.component.ts +++ b/src/main/webapp/app/layouts/error/error.component.ts @@ -15,7 +15,6 @@ export class ErrorComponent implements OnInit, OnDestroy { error403: boolean; modalRef: NgbModalRef; private routeSubscription: Subscription; - private loginUrl = 'oauth/login'; constructor( private loginModalService: LoginModalService, @@ -35,6 +34,6 @@ export class ErrorComponent implements OnInit, OnDestroy { } login() { - window.location.href = this.loginUrl; + window.location.href = ''; } } \ No newline at end of file From 57419935186e7b97249a526294da4836bd5bb335 Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 23 Aug 2024 15:43:12 +0100 Subject: [PATCH 22/22] Remove unused TokenKeyEndpoint --- .../config/SecurityConfiguration.kt | 2 +- .../management/web/rest/TokenKeyEndpoint.kt | 30 ------------------- 2 files changed, 1 insertion(+), 31 deletions(-) delete mode 100644 src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt diff --git a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt index 230b39652..abe0ad339 100644 --- a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt +++ b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt @@ -5,7 +5,7 @@ import org.radarbase.auth.jwks.JwkAlgorithmParser import org.radarbase.auth.jwks.JwksTokenVerifierLoader import org.radarbase.management.repository.UserRepository import org.radarbase.management.security.Http401UnauthorizedEntryPoint -import org.radarbase.management.security.JwtAuthenticationFilter // Make sure to import this +import org.radarbase.management.security.JwtAuthenticationFilter import org.radarbase.management.security.RadarAuthenticationProvider import org.springframework.beans.factory.BeanInitializationException import org.springframework.beans.factory.annotation.Autowired diff --git a/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt deleted file mode 100644 index 2e7e5c400..000000000 --- a/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.radarbase.management.web.rest - -import io.micrometer.core.annotation.Timed -import org.radarbase.auth.jwks.JsonWebKeySet -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RestController - -//@RestController -class TokenKeyEndpoint @Autowired constructor( -) { - @get:Timed - @get:GetMapping("/oauth/token_key") - val key: JsonWebKeySet? - /** - * Get the verification key for the token signatures. The principal has to - * be provided only if the key is secret - * - * @return the key used to verify tokens - */ - get() { - logger.debug("Requesting verifier public keys...") - return null - } - - companion object { - private val logger = LoggerFactory.getLogger(TokenKeyEndpoint::class.java) - } -}