Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change login flow to use Ory #924

Merged
merged 29 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ad6c73e
Update front end to use auth code login
mpgxvii Aug 5, 2024
8c9fec3
Add login endpoint to allow auth code grant login
mpgxvii Aug 5, 2024
c09291c
Add token validator updates to support hydra tokens
mpgxvii Aug 5, 2024
4820edc
Add auth server config to application properties
mpgxvii Aug 6, 2024
6beedc4
Fix access token converter
mpgxvii Aug 6, 2024
0590f33
Merge branch 'hydra-kratos-stack' of https://github.com/RADAR-base/Ma…
mpgxvii Aug 13, 2024
d0d61aa
Fix application properties
mpgxvii Aug 13, 2024
5234fd2
Remove unused KratosTokenVerifier and Oauth2LoginUiWebConfig
mpgxvii Aug 14, 2024
ea47775
Update setting of hydra token verifier loader
mpgxvii Aug 14, 2024
39b06b5
Merge remote-tracking branch 'origin/feature/ory-based-authorization'…
yatharthranjan Aug 14, 2024
1a9daca
add separate login url for hydra
yatharthranjan Aug 19, 2024
bc2b2f6
Update ManagementPortal propreties with new loginUrl property
mpgxvii Aug 19, 2024
57eecfd
intermediate removal of unneeded components
yatharthranjan Aug 20, 2024
03c87ed
Fix ory stack configs
mpgxvii Aug 20, 2024
83087eb
Merge branch 'feat/hydra-token' of https://github.com/RADAR-base/Mana…
mpgxvii Aug 20, 2024
1882851
Remove OAuth2ServerConfiguration and use single SecurityConfig for auth
mpgxvii Aug 22, 2024
7600ae8
Refactor JwtAuthenticationFilter to accept jwt and session data
mpgxvii Aug 22, 2024
54aa7ff
Remove unused services
mpgxvii Aug 22, 2024
5e6f20d
Restore ory stack changes
mpgxvii Aug 22, 2024
eaa7d33
Update JwtAuthenticationFilter issues: make sure existing auth is che…
mpgxvii Aug 23, 2024
aa6b6f0
Merge branch 'feature/ory-based-authorization' of https://github.com/…
mpgxvii Aug 23, 2024
960a3b5
Restore deleted annotations
mpgxvii Aug 23, 2024
c11734e
Move access token fetching to AuthService
mpgxvii Aug 23, 2024
2988114
Invalidate session on logout
mpgxvii Aug 23, 2024
51b5fdd
Merge branch 'feature/ory-based-authorization' of https://github.com/…
mpgxvii Aug 23, 2024
2c4683c
Fix error component login url and remove unnecessary logs
mpgxvii Aug 23, 2024
393bcf9
Merge branch 'feature/ory-based-authorization' of https://github.com/…
mpgxvii Aug 23, 2024
b4ec9b2
Merge branch 'feature/ory-based-authorization' of https://github.com/…
mpgxvii Aug 23, 2024
5741993
Remove unused TokenKeyEndpoint
mpgxvii Aug 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -34,6 +36,10 @@ public IdentityServer getIdentityServer() {
return identityServer;
}

public AuthServer getAuthServer() {
return authServer;
}

public ManagementPortalProperties.Mail getMail() {
return mail;
}
Expand Down Expand Up @@ -324,6 +330,28 @@ public void setLoginUrl(String loginUrl) {
}
}

public class AuthServer {
private String serverUrl = null;
private String serverAdminUrl = null;

public String getServerUrl() {
return serverUrl;
}

public void setServerUrl(String serverUrl) {
this.serverUrl = serverUrl;
}

public String getServerAdminUrl() {
return serverAdminUrl;
}

public void setServerAdminUrl(String serverAdminUrl) {
this.serverAdminUrl = serverAdminUrl;
}
}


public static class CatalogueServer {

private boolean enableAutoImport = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ class OAuth2ServerConfiguration(
fun accessTokenConverter(): ManagementPortalJwtAccessTokenConverter {
logger.debug("loading token converter from keystore configurations")
return ManagementPortalJwtAccessTokenConverter(
keyStoreHandler.tokenValidator,
keyStoreHandler.algorithmForSigning,
keyStoreHandler.verifiers,
keyStoreHandler.refreshTokenVerifiers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.auth0.jwt.exceptions.SignatureVerificationException
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.ObjectMapper
import org.radarbase.management.security.jwt.ManagementPortalJwtAccessTokenConverter
import org.radarbase.auth.authentication.TokenValidator
import org.slf4j.LoggerFactory
import org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken
Expand All @@ -33,6 +34,7 @@ import java.util.stream.Stream
* are significantly smaller than RSA signatures.
*/
open class ManagementPortalJwtAccessTokenConverter(
validator: TokenValidator,
algorithm: Algorithm,
verifiers: MutableList<JWTVerifier>,
private val refreshTokenVerifiers: List<JWTVerifier>
Expand All @@ -59,6 +61,7 @@ open class ManagementPortalJwtAccessTokenConverter(
field = jwtClaimsSetVerifier
}
private var algorithm: Algorithm? = null
private var validator: TokenValidator
private val verifiers: MutableList<JWTVerifier>

/**
Expand All @@ -72,6 +75,7 @@ open class ManagementPortalJwtAccessTokenConverter(
accessToken.setIncludeGrantType(true)
tokenConverter = accessToken
this.verifiers = verifiers
this.validator = validator
setAlgorithm(algorithm)
}

Expand Down Expand Up @@ -229,7 +233,7 @@ open class ManagementPortalJwtAccessTokenConverter(
}
for (verifier in verifierToUse) {
try {
verifier.verify(token)
validator.validateBlocking(token)
return claims
} catch (sve: SignatureVerificationException) {
logger.warn("Client presented a token with an incorrect signature")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class ManagementPortalOauthKeyStoreHandler @Autowired constructor(
private val oauthConfig: Oauth
private val verifierPublicKeyAliasList: List<String>
private val managementPortalBaseUrl: String
private val authServerUrl: String
val verifiers: MutableList<JWTVerifier>
val refreshTokenVerifiers: MutableList<JWTVerifier>

Expand Down Expand Up @@ -79,6 +80,8 @@ class ManagementPortalOauthKeyStoreHandler @Autowired constructor(
// No need to check audience with a refresh token: it can be used
// to refresh tokens intended for other resources.
refreshTokenVerifiers = algorithms.map { algo: Algorithm -> JWT.require(algo).build() }.toMutableList()
authServerUrl = managementPortalProperties.authServer.serverAdminUrl
tokenValidator.refresh()
}

@Nonnull
Expand Down Expand Up @@ -228,7 +231,11 @@ class ManagementPortalOauthKeyStoreHandler @Autowired constructor(
RES_MANAGEMENT_PORTAL,
JwkAlgorithmParser()
),
KratosTokenVerifierLoader(managementPortalProperties.identityServer.publicUrl(), requireAal2 = managementPortalProperties.oauth.requireAal2),
JwksTokenVerifierLoader(
authServerUrl + "/admin/keys/hydra.jwt.access-token",
RES_MANAGEMENT_PORTAL,
JwkAlgorithmParser()
),
)
return TokenValidator(loaderList)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,112 @@
package org.radarbase.management.web.rest

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.jsonPrimitive
import org.radarbase.auth.exception.IdpException
import org.radarbase.management.config.ManagementPortalProperties
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.servlet.view.RedirectView
import java.time.Duration
import java.time.Instant

@RestController
@RequestMapping("/api")
class LoginEndpoint
@Autowired
constructor(
@Autowired private val managementPortalProperties: ManagementPortalProperties,
private val managementPortalProperties: ManagementPortalProperties,
) {
private val httpClient =
HttpClient(CIO) {
install(HttpTimeout) {
connectTimeoutMillis = Duration.ofSeconds(10).toMillis()
socketTimeoutMillis = Duration.ofSeconds(10).toMillis()
requestTimeoutMillis = Duration.ofSeconds(300).toMillis()
}
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
}

@GetMapping("/redirect/login")
fun loginRedirect(): RedirectView {
suspend fun loginRedirect(
@RequestParam(required = false) code: String?,
): RedirectView {
val redirectView = RedirectView()
redirectView.url = managementPortalProperties.identityServer.loginUrl +
"/login?return_to=" + managementPortalProperties.common.managementPortalBaseUrl
val config = managementPortalProperties
val mpUrl = config.common.baseUrl

if (code == null) {
redirectView.url = buildAuthUrl(config, mpUrl)
} else {
val accessToken = fetchAccessToken(code, config)
redirectView.url = "$mpUrl/#/?access_token=$accessToken"
}
return redirectView
}

@GetMapping("/redirect/account")
fun settingsRedirect(): RedirectView {
val redirectView = RedirectView()
redirectView.url = managementPortalProperties.identityServer.loginUrl + "/settings"
redirectView.url = "${managementPortalProperties.identityServer.loginUrl}/settings"
return redirectView
}

private fun buildAuthUrl(config: ManagementPortalProperties, mpUrl: String): String {
return "${config.authServer.serverUrl}/oauth2/auth?" +
"client_id=${config.frontend.clientId}&" +
"response_type=code&" +
"state=${Instant.now()}&" +
"audience=res_ManagementPortal&" +
"scope=offline&" +
"redirect_uri=$mpUrl/api/redirect/login"
}

private suspend fun fetchAccessToken(
mpgxvii marked this conversation as resolved.
Show resolved Hide resolved
code: String,
config: ManagementPortalProperties,
): String {
val tokenUrl = "${config.authServer.serverUrl}/oauth2/token"
val response =
httpClient.post(tokenUrl) {
contentType(ContentType.Application.FormUrlEncoded)
accept(ContentType.Application.Json)
setBody(
Parameters
.build {
append("grant_type", "authorization_code")
append("code", code)
append("redirect_uri", "${config.common.baseUrl}/api/redirect/login")
append("client_id", config.frontend.clientId)
}.formUrlEncode(),
)
}

if (response.status.isSuccess()) {
val responseMap = response.body<Map<String, JsonElement>>()
return responseMap["access_token"]?.jsonPrimitive?.content
?: throw IdpException("Access token not found in response")
} else {
throw IdpException("Unable to get access token")
}
}

companion object {
private val logger = LoggerFactory.getLogger(TokenKeyEndpoint::class.java)
private val logger = LoggerFactory.getLogger(LoginEndpoint::class.java)
}
}
7 changes: 6 additions & 1 deletion src/main/resources/config/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,14 @@ managementportal:
# The line below can be uncommented to add some hidden fields for UI testing
#hiddenSubjectFields: [person_name, date_of_birth, group]
identityServer:
adminEmail: admin-email-here@gmail.com
serverUrl: http://localhost:4433
serverAdminUrl: http://localhost:4434
serverAdminUrl: http://kratos-admin
loginUrl: http://localhost:3000
authServer:
serverUrl: http://localhost:4444
serverAdminUrl: http://localhost:4445


# ===================================================================
# JHipster specific properties
Expand Down
52 changes: 20 additions & 32 deletions src/main/webapp/app/home/home.component.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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
}
}
Loading
Loading