diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f79e191d8..0a9f89859 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,7 +4,7 @@ name: Main # Run in master and dev branches and in all pull requests to those branches on: push: - branches: [ master, dev ] + branches: [ master, dev, feature/* ] pull_request: {} env: diff --git a/gradle.properties b/gradle.properties index 6684b33e0..34bb975dd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,7 +16,7 @@ thymeleaf_version=3.1.2.RELEASE spring_session_version=2021.2.0 gatling_version=3.8.4 mapstruct_version=1.5.5.Final -jackson_version=2.16.1 +jackson_version=2.15.0 javax_xml_bind_version=2.3.3 javax_jaxb_core_version=2.3.0.1 javax_jaxb_runtime_version=2.3.8 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/radar-auth/src/main/java/org/radarbase/auth/token/RadarToken.kt b/radar-auth/src/main/java/org/radarbase/auth/token/RadarToken.kt index 03910caf4..d9c99950b 100644 --- a/radar-auth/src/main/java/org/radarbase/auth/token/RadarToken.kt +++ b/radar-auth/src/main/java/org/radarbase/auth/token/RadarToken.kt @@ -98,7 +98,7 @@ interface RadarToken { * @return true if the client credentials flow was certainly used, false otherwise. */ val isClientCredentials: Boolean - get() = grantType == CLIENT_CREDENTIALS + get() = grantType == CLIENT_CREDENTIALS || (subject != null && subject == clientId) fun copyWithRoles(roles: Set): RadarToken diff --git a/src/main/docker/etc/config/kratos/kratos.yml b/src/main/docker/etc/config/kratos/kratos.yml index 99a36d1f1..d194bcfc3 100644 --- a/src/main/docker/etc/config/kratos/kratos.yml +++ b/src/main/docker/etc/config/kratos/kratos.yml @@ -2,12 +2,12 @@ dsn: memory serve: public: - base_url: http://127.0.0.1:4433/ + base_url: http://localhost:4433/ admin: base_url: http://kratos:4434/ selfservice: - default_browser_return_url: http://127.0.0.1:3000/ + default_browser_return_url: http://localhost:3000/ allowed_return_urls: - "http://127.0.0.1:3000/" - "http://127.0.0.1:8080/" @@ -19,23 +19,6 @@ selfservice: methods: password: enabled: true - # oidc: - # config: - # providers: - # # social sign-in for google. This needs to be tied to a google account. values below were added by bastiaan - # - id: google_d292689d # this is `` in the Authorization callback URL. DO NOT CHANGE IT ONCE SET! current google callback: http://127.0.0.1:4433/self-service/methods/oidc/callback/google_d292689d - # provider: google - # client_id: 922854293804-r3fhl9tom6uutcq5c8fm4592l1t6s3mh.apps.googleusercontent.com # Replace this with the Client ID - # client_secret: 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 # Replace this with the Client secret - # issuer_url: https://accounts.google.com # Replace this with the providers issuer URL - # mapper_url: "base64://bG9jYWwgY2xhaW1zID0gewogIGVtYWlsX3ZlcmlmaWVkOiBmYWxzZSwKfSArIHN0ZC5leHRWYXIoJ2NsYWltcycpOwoKewogIGlkZW50aXR5OiB7CiAgICB0cmFpdHM6IHsKICAgICAgW2lmICdlbWFpbCcgaW4gY2xhaW1zICYmIGNsYWltcy5lbWFpbF92ZXJpZmllZCB0aGVuICdlbWFpbCcgZWxzZSBudWxsXTogY2xhaW1zLmVtYWlsLAogICAgfSwKICB9LAp9" - # # currently: GitHub example from: https://www.ory.sh/docs/kratos/social-signin/data-mapping - # # Alternatively, use an URL: - # # mapper_url: https://storage.googleapis.com/abc-cde-prd/9cac9717f007808bf17 - # scope: - # - email - # # supported scopes can be found in your providers dev docs - # enabled: true totp: config: issuer: Kratos @@ -45,34 +28,34 @@ selfservice: flows: error: - ui_url: http://127.0.0.1:3000/error + ui_url: http://localhost:3000/error settings: - ui_url: http://127.0.0.1:3000/settings - + ui_url: http://localhost:3000/settings + recovery: enabled: true - ui_url: http://127.0.0.1:3000/recovery - use: link + ui_url: http://localhost:3000/recovery + use: code verification: # our current flow necessitates that users reset their password after they activate an account in managementportal, # this works as verification - ui_url: http://127.0.0.1:3000/verification + ui_url: http://localhost:3000/verification enabled: true - use: link + use: code after: - default_browser_return_url: http://127.0.0.1:3000 + default_browser_return_url: http://localhost:3000/consent logout: after: - default_browser_return_url: http://127.0.0.1:3000/login + default_browser_return_url: http://localhost:3000/login login: - ui_url: http://127.0.0.1:3000/login + ui_url: http://localhost:3000/login registration: - ui_url: http://127.0.0.1:3000/registration + ui_url: http://localhost:3000/registration after: password: hooks: @@ -104,3 +87,6 @@ courier: smtp: connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true&disable_starttls=true from_address: noreply@radar-base.org + +oauth2_provider: + url: http://hydra:4445 \ No newline at end of file diff --git a/src/main/docker/etc/config/kratos/webhook_body.jsonnet b/src/main/docker/etc/config/kratos/webhook_body.jsonnet new file mode 100644 index 000000000..3af9998b2 --- /dev/null +++ b/src/main/docker/etc/config/kratos/webhook_body.jsonnet @@ -0,0 +1,5 @@ +function(ctx) { + identity: if std.objectHas(ctx, "identity") then ctx.identity else null, + payload: if std.objectHas(ctx, "flow") && std.objectHas(ctx.flow, "transient_payload") then ctx.flow.transient_payload else null, + cookies: ctx.request_cookies +} diff --git a/src/main/docker/etc/postgres/init-user-db.sh b/src/main/docker/etc/postgres/init-user-db.sh new file mode 100755 index 000000000..85a57e75f --- /dev/null +++ b/src/main/docker/etc/postgres/init-user-db.sh @@ -0,0 +1,34 @@ +#! /bin/bash + + set -e + set -u + export PGPASSWORD="$POSTGRES_PASSWORD" + export PGUSER="$POSTGRES_USER" + + function create_user_and_database() { + export PGPASSWORD="$POSTGRES_PASSWORD" + export PGUSER="$POSTGRES_USER" + local database=$1 + local database_exist=$(psql -U $PGUSER -tAc "SELECT 1 FROM pg_database WHERE datname='$database';") + if [[ "$database_exist" == 1 ]]; then + echo "Database $database already exists" + else + echo "Database $database does not exist" + echo " Creating database '$database' for user '$PGUSER'" + + psql -U $PGUSER -v ON_ERROR_STOP=1 <<-EOSQL + CREATE DATABASE "$database"; + GRANT ALL PRIVILEGES ON DATABASE $database TO $PGUSER; +EOSQL + fi + } + + if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then + echo "Multiple database creation requested: $POSTGRES_MULTIPLE_DATABASES" + #waiting for postgres + for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do + create_user_and_database $db + done + echo "Databases created" + fi + diff --git a/src/main/docker/managementportal.yml b/src/main/docker/managementportal.yml index 5487f5cb7..ad492395c 100644 --- a/src/main/docker/managementportal.yml +++ b/src/main/docker/managementportal.yml @@ -14,6 +14,9 @@ services: - MANAGEMENTPORTAL_IDENTITYSERVER_SERVERURL=http://kratos:4433 - 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 ports: diff --git a/src/main/docker/non_managementportal/docker-compose.yml b/src/main/docker/non_managementportal/docker-compose.yml index 3497d6c90..68a2f0291 100644 --- a/src/main/docker/non_managementportal/docker-compose.yml +++ b/src/main/docker/non_managementportal/docker-compose.yml @@ -13,6 +13,9 @@ networks: driver: bridge internal: true +volumes: + pgdata: + services: managementportal-postgresql: extends: @@ -22,10 +25,10 @@ services: - db - default - kratos-selfservice-ui-node: + radar-self-enrolment-ui: extends: file: ../ory_stack.yml - service: kratos-selfservice-ui-node + service: radar-self-enrolment-ui networks: - ory - default @@ -46,10 +49,10 @@ services: networks: - ory - postgresd-kratos: + postgresd-ory: extends: file: ../ory_stack.yml - service: postgresd-kratos + service: postgresd-ory networks: - ory @@ -60,3 +63,19 @@ services: networks: - ory - default + + hydra-migrate: + extends: + file: ../ory_stack.yml + service: hydra-migrate + networks: + - ory + + hydra: + extends: + file: ../ory_stack.yml + service: hydra + networks: + - ory + - default + diff --git a/src/main/docker/ory_stack.yml b/src/main/docker/ory_stack.yml index b95835b53..3bad1585e 100644 --- a/src/main/docker/ory_stack.yml +++ b/src/main/docker/ory_stack.yml @@ -1,18 +1,15 @@ version: '3.8' +volumes: + pgdata: + services: - kratos-selfservice-ui-node: - image: - oryd/kratos-selfservice-ui-node + radar-self-enrolment-ui: + image: ghcr.io/radar-base/radar-self-enrolment-ui:dev environment: - - LOG_LEAK_SENSITIVE_VALUES=true - - KRATOS_PUBLIC_URL=http://kratos:4433 - - KRATOS_ADMIN_URL=http://kratos:4434 - - SECURITY_MODE=standalone - - KRATOS_BROWSER_URL=http://127.0.0.1:4433 - - COOKIE_SECRET=unsafe_cookie_secret - - CSRF_COOKIE_NAME=radar - - CSRF_COOKIE_SECRET=unsafe_csrf_cookie_secret + - ORY_SDK_URL=http://kratos:4433 + - HYDRA_ADMIN_URL=http://hydra:4445 + - HYDRA_PUBLIC_URL=http://hydra:4444 ports: - "3000:3000" volumes: @@ -27,7 +24,18 @@ services: - "4434:4434" # admin, should be closed in production restart: unless-stopped environment: - - DSN=postgres://kratos:secret@postgresd-kratos/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 + - 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: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:8080/managementportal/api/kratos/subjects/activate + - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_BODY=/etc/config/kratos/webhook_body.jsonnet + - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_RESPONSE_IGNORE=true command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier volumes: - type: bind @@ -38,7 +46,7 @@ services: image: oryd/kratos:v1.0.0 environment: - - DSN=postgres://kratos:secret@postgresd-kratos/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 + - DSN=postgres://ory:secret@postgresd-ory/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 volumes: - type: bind source: ./etc/config/kratos @@ -46,15 +54,56 @@ services: command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes restart: on-failure - postgresd-kratos: + postgresd-ory: image: postgres:11.8 environment: - - POSTGRES_USER=kratos + - POSTGRES_USER=ory - POSTGRES_PASSWORD=secret - - POSTGRES_DB=kratos + - POSTGRES_MULTIPLE_DATABASES=kratos,hydra + volumes: + - pgdata:/var/lib/postgresql/data + - ./etc/postgres/init-user-db.sh:/docker-entrypoint-initdb.d/init-user-db.sh mailslurper: image: oryd/mailslurper:latest-smtps ports: - "4436:4436" - "4437:4437" + + hydra-migrate: + image: oryd/hydra:v2.2.0 + environment: + - DSN=postgres://ory:secret@postgresd-ory/hydra?sslmode=disable&max_conns=20&max_idle_conns=4 + command: migrate sql -e --yes + restart: on-failure + + hydra: + image: oryd/hydra:v2.2.0 + depends_on: + - hydra-migrate + ports: + - "4444:4444" # Public port + - "4445:4445" # Admin port + - "5555:5555" # Port for hydra token user + command: + serve all --dev + restart: on-failure # TODO figure out why we need this (incorporate health check into hydra migrate command?) + environment: + - DSN=postgres://ory:secret@postgresd-ory/hydra?sslmode=disable&max_conns=20&max_idle_conns=4 + - LOG_LEAK_SENSITIVE_VALUES=true + - URLS_SELF_ISSUER=http://localhost:4444 + - URLS_SELF_PUBLIC=http://localhost:4444 + - URLS_CONSENT=http://localhost:3000/consent + - URLS_LOGIN=http://localhost:3000/login + - URLS_LOGOUT=http://localhost:3000/logout + - URLS_IDENTITY_PROVIDER_PUBLICURL=http://localhost:4433 + - URLS_IDENTITY_PROVIDER_URL=http://localhost:4434 + - SECRETS_SYSTEM=youReallyNeedToChangeThis + - OIDC_SUBJECT_IDENTIFIERS_SUPPORTED_TYPES=public,pairwise + - OIDC_SUBJECT_IDENTIFIERS_PAIRWISE_SALT=youReallyNeedToChangeThis + - STRATEGIES_ACCESS_TOKEN=jwt + - STRATEGIES_JWT_SCOPE_CLAIM=both + - SERVE_PUBLIC_CORS_ENABLED=true + - SERVE_ADMIN_CORS_ENABLED=true + - OAUTH2_ALLOWED_TOP_LEVEL_CLAIMS=scope,roles,authorities,sources,user_name + - OAUTH2_MIRROR_TOP_LEVEL_CLAIMS=false diff --git a/src/main/java/org/radarbase/management/config/ManagementPortalProperties.java b/src/main/java/org/radarbase/management/config/ManagementPortalProperties.java index 520bd1d69..8518f9554 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,37 @@ 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; + } + + public void setServerUrl(String serverUrl) { + this.serverUrl = serverUrl; + } + + public String getServerAdminUrl() { + return serverAdminUrl; + } + + public void setServerAdminUrl(String serverAdminUrl) { + this.serverAdminUrl = serverAdminUrl; + } + + public String getLoginUrl() { + return loginUrl; + } + + public void setLoginUrl(String loginUrl) { + this.loginUrl = loginUrl; + } + } + + public static class CatalogueServer { private boolean enableAutoImport = false; 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 - ) - } -} 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 2d1238e2a..000000000 --- a/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt +++ /dev/null @@ -1,293 +0,0 @@ -package org.radarbase.management.config - -import java.util.* -import javax.sql.DataSource -import org.radarbase.auth.authorization.RoleAuthority -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 keyStoreHandler: ManagementPortalOauthKeyStoreHandler - ) { - @Bean - fun jwtAuthenticationFilter(): JwtAuthenticationFilter { - return JwtAuthenticationFilter( - keyStoreHandler.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 http401UnauthorizedEntryPoint: Http401UnauthorizedEntryPoint, - @Autowired private val logoutSuccessHandler: LogoutSuccessHandler, - @Autowired private val authenticationManager: AuthenticationManager, - @Autowired private val userRepository: UserRepository - ) : ResourceServerConfigurerAdapter() { - - fun jwtAuthenticationFilter(): JwtAuthenticationFilter { - return JwtAuthenticationFilter( - keyStoreHandler.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 keyStoreHandler: ManagementPortalOauthKeyStoreHandler - ) : AuthorizationServerConfigurerAdapter() { - - @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( - keyStoreHandler.algorithmForSigning, - keyStoreHandler.verifiers, - 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(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..abe0ad339 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 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,121 @@ 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()) + .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/*") + .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/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") + .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() + .and() + .logout().invalidateHttpSession(true) + .logoutUrl("/api/logout") + } + + @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/JwtAuthenticationFilter.kt b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt index f667c33f8..d60b5e8ab 100644 --- a/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt +++ b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt @@ -10,47 +10,26 @@ 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 - -/** - * 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)) @@ -60,48 +39,70 @@ class JwtAuthenticationFilter @JvmOverloads constructor( @Throws(IOException::class, ServletException::class) override fun doFilterInternal( - httpRequest: HttpServletRequest, - httpResponse: HttpServletResponse, - chain: FilterChain, + httpRequest: HttpServletRequest, + httpResponse: HttpServletResponse, + chain: FilterChain ) { - if (CorsUtils.isPreFlightRequest(httpRequest)) { - Companion.logger.debug("Skipping JWT check for preflight request") - chain.doFilter(httpRequest, httpResponse) - return - } - - val existingAuthentication = SecurityContextHolder.getContext().authentication - - if (existingAuthentication.isAnonymous || existingAuthentication is OAuth2Authentication) { - val session = httpRequest.getSession(false) + 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" - token = session?.radarToken - ?.takeIf { Instant.now() < it.expiresAt } - if (token != null) { - Companion.logger.debug("Using token from session") - } - else if (stringToken != null) { + + if (stringToken != null) { try { + logger.warn("Validating token from header: $stringToken") token = validator.validateBlocking(stringToken) - Companion.logger.debug("Using token from header") + val authentication = createAuthenticationFromToken(token) + SecurityContextHolder.getContext().authentication = authentication + logger.debug("JWT authentication successful") } catch (ex: TokenValidationException) { - ex.message?.let { exMessage = it } - Companion.logger.info("Failed to validate token from header: {}", exMessage) + exMessage = ex.message ?: exMessage + logger.info("Failed to validate token from header: $exMessage") + } + } + + if (token == null) { + val existingAuthentication = SecurityContextHolder.getContext().authentication + if (existingAuthentication != null && + existingAuthentication.isAuthenticated && + !existingAuthentication.isAnonymous + ) { + 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 (!validateToken(token, httpRequest, httpResponse, session, exMessage)) { + + 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) } 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) + logger.debug("Skipping JWT check for ${httpRequest.requestURI}") true } else { false @@ -109,74 +110,44 @@ class JwtAuthenticationFilter @JvmOverloads constructor( } private fun tokenFromHeader(httpRequest: HttpServletRequest): String? { - 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 + return httpRequest + .getHeader(HttpHeaders.AUTHORIZATION) + ?.takeIf { it.startsWith(AUTHORIZATION_BEARER_HEADER) } + ?.removePrefix(AUTHORIZATION_BEARER_HEADER) + ?.trim { it <= ' ' } } - @Throws(IOException::class) private fun validateToken( - token: RadarToken?, - httpRequest: HttpServletRequest, - httpResponse: HttpServletResponse, - session: HttpSession?, - exMessage: String?, + 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) + httpRequest.radarToken = token + val authentication = RadarAuthentication(token) authenticationManager.authenticate(authentication) SecurityContextHolder.getContext().authentication = authentication true } else if (isOptional) { - logger.debug("Skipping optional token") + logger.debug("Skipping optional token check for ${httpRequest.requestURI}") true } else { - logger.error("Unauthorized - no valid token provided") - httpResponse.returnUnauthorized(httpRequest, exMessage) + logger.error("Unauthorized - no valid token provided for ${httpRequest.requestURI}") + httpResponse.returnUnauthorized(httpRequest) 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 - } - } - companion object { - 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) - val fullMessage = if (message != null) { - "\"$message\"" - } else { - "null" - } outputStream.print( - """ + """ {"error": "Unauthorized", "status": "${HttpServletResponse.SC_UNAUTHORIZED}", - message": $fullMessage, "path": "${request.requestURI}"} - """.trimIndent() + """.trimIndent() ) } @@ -184,25 +155,6 @@ class JwtAuthenticationFilter @JvmOverloads constructor( private const val AUTHORIZATION_BEARER_HEADER = "Bearer" private const val TOKEN_ATTRIBUTE = "jwt" - /** - * 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? @@ -219,7 +171,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/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 a57826d0a..000000000 --- a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt +++ /dev/null @@ -1,250 +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.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 - -/** - * 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( - 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 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 - 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) - } - for (verifier in verifierToUse) { - try { - verifier.verify(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 - ) - } - } - 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/security/jwt/ManagementPortalOauthKeyStoreHandler.kt b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt deleted file mode 100644 index 752a295f1..000000000 --- a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt +++ /dev/null @@ -1,296 +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.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 -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(ManagementPortalJwtAccessTokenConverter.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() - } - - @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() - ), - KratosTokenVerifierLoader(managementPortalProperties.identityServer.publicUrl(), requireAal2 = managementPortalProperties.oauth.requireAal2), - ) - 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/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/service/MetaTokenService.kt b/src/main/java/org/radarbase/management/service/MetaTokenService.kt deleted file mode 100644 index 529011bd7..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 7f09ce8f7..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 3fae78093..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/LoginEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt index 12017d8ad..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,36 +1,54 @@ package org.radarbase.management.web.rest +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 @RestController @RequestMapping("/api") class LoginEndpoint - @Autowired - constructor( - @Autowired private val managementPortalProperties: ManagementPortalProperties, - ) { - @GetMapping("/redirect/login") - fun loginRedirect(): RedirectView { - val redirectView = RedirectView() - redirectView.url = managementPortalProperties.identityServer.loginUrl + - "/login?return_to=" + managementPortalProperties.common.managementPortalBaseUrl - return redirectView - } +@Autowired +constructor( + private val managementPortalProperties: ManagementPortalProperties, + @Autowired private val authService: AuthService +) { - @GetMapping("/redirect/account") - fun settingsRedirect(): RedirectView { - val redirectView = RedirectView() - redirectView.url = managementPortalProperties.identityServer.loginUrl + "/settings" - return redirectView - } + @GetMapping("/redirect/login") + suspend fun loginRedirect( + @RequestParam(required = false) code: String?, + ): RedirectView { + val redirectView = RedirectView() - companion object { - private val logger = LoggerFactory.getLogger(TokenKeyEndpoint::class.java) + if (code == null) { + redirectView.url = buildAuthUrl() + } else { + val accessToken = authService.fetchAccessToken(code) + redirectView.url = + "${managementPortalProperties.common.baseUrl}/#/?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(): 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" } +} 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 bbf0a0b2b..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 ef7417585..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/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 c4fc357ee..000000000 --- a/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt +++ /dev/null @@ -1,32 +0,0 @@ -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 -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 - /** - * 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 keyStoreHandler.loadJwks() - } - - companion object { - private val logger = LoggerFactory.getLogger(TokenKeyEndpoint::class.java) - } -} diff --git a/src/main/resources/config/application-dev.yml b/src/main/resources/config/application-dev.yml index 87e51ec0d..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 @@ -116,9 +116,15 @@ 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 + loginUrl: http://localhost:4444 + # =================================================================== # JHipster specific properties diff --git a/src/main/resources/config/application-prod.yml b/src/main/resources/config/application-prod.yml index 5fa817e8b..01ae180aa 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: @@ -89,7 +89,7 @@ managementportal: from: ManagementPortal@localhost frontend: clientId: ManagementPortalapp - clientSecret: + clientSecret: secret accessTokenValiditySeconds: 14400 refreshTokenValiditySeconds: 259200 sessionTimeout: 86400 @@ -101,9 +101,13 @@ 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://hydra:4444 + serverAdminUrl: http://hydra:4445 + loginUrl: http://localhost:4444 # =================================================================== # JHipster specific properties 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/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 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); } } diff --git a/src/test/java/org/radarbase/auth/authentication/OAuthHelper.kt b/src/test/java/org/radarbase/auth/authentication/OAuthHelper.kt index 0ab493a0f..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() @@ -128,7 +127,7 @@ object OAuthHelper { Mockito.`when`(userRepository.findOneByLogin(ArgumentMatchers.anyString())).thenReturn( createAdminUser() ) - return JwtAuthenticationFilter(createTokenValidator(), { auth: Authentication? -> auth }, userRepository) + return JwtAuthenticationFilter(createTokenValidator(), { auth: Authentication? -> auth }) } /** @@ -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()) - } -}