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

multiple passport brokers & passport refresh token #718

Merged
merged 3 commits into from
Jul 5, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
26 changes: 18 additions & 8 deletions src/main/java/bio/overture/ego/controller/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Objects;
import java.util.Optional;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -64,8 +65,7 @@ public class AuthController {
private final GoogleTokenService googleTokenService;
private final TokenSigner tokenSigner;
private final RefreshContextService refreshContextService;

private final String PASSPORT_CLIENT_NAME = "passport";
private final String GA4GH_PASSPORT_SCOPE = "ga4gh_passport_v1";

@Autowired
public AuthController(
Expand Down Expand Up @@ -128,22 +128,32 @@ public ResponseEntity<String> user(

val user = (CustomOAuth2User) authentication.getPrincipal();

val passportJwtToken = (authentication.getAuthorizedClientRegistrationId().equals(PASSPORT_CLIENT_NAME)) ?
passportService.getPassportToken(((CustomOAuth2User) authentication.getPrincipal()).getAccessToken()) :

val passportJwtToken = (user.getClaim(GA4GH_PASSPORT_SCOPE) != null) ?
passportService.getPassportToken(
authentication.getAuthorizedClientRegistrationId(),
user.getAccessToken()) :
null;

Optional<ProviderType> providerType = ProviderType
.findIfExist(authentication.getAuthorizedClientRegistrationId());

if(user.getClaim(GA4GH_PASSPORT_SCOPE) != null && providerType.isEmpty()){
providerType = Optional.of(ProviderType.PASSPORT);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assigning a generic providerType "PASSPORT" to any passport provider that contains the scope ga4gh_passport_v1


String token =
tokenService.generateUserToken(
IDToken.builder()
.providerSubjectId(user.getSubjectId())
.email(user.getEmail())
.familyName(user.getFamilyName())
.givenName(user.getGivenName())
.providerType(
ProviderType.resolveProviderType(
authentication.getAuthorizedClientRegistrationId()))
.providerType(providerType.get())
.providerIssuerUri(user.getIssuer().toString())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a new field providerIssuerUri to differentiate passport brokers

.build(),
passportJwtToken);
passportJwtToken,
authentication.getAuthorizedClientRegistrationId());

val outgoingRefreshContext = refreshContextService.createInitialRefreshContext(token);
val cookie =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,6 @@ public class CreateUserRequest {
@NotNull ProviderType providerType;

@NotNull String providerSubjectId;

private String providerIssuerUri;
}
4 changes: 4 additions & 0 deletions src/main/java/bio/overture/ego/model/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ public class User implements PolicyOwner, Identifiable<UUID> {
@Column(name = SqlFields.PROVIDERSUBJECTID, nullable = false)
private String providerSubjectId;

@JsonView({Views.JWTAccessToken.class, Views.REST.class})
@Column(name = SqlFields.PROVIDERISSUERURI)
private String providerIssuerUri;

@JsonIgnore
@OneToMany(
mappedBy = JavaFields.OWNER,
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/bio/overture/ego/model/enums/ProviderType.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

import java.util.Optional;

@RequiredArgsConstructor
public enum ProviderType {
GOOGLE,
Expand Down Expand Up @@ -53,6 +55,12 @@ public static ProviderType resolveProviderType(@NonNull String providerType) {
providerType, COMMA.join(values()))));
}

public static Optional<ProviderType> findIfExist(@NonNull String providerType) {
return stream(values())
.filter(x -> x.toString().equalsIgnoreCase(providerType))
.findFirst();
}

@Override
public String toString() {
return this.name();
Expand Down
1 change: 1 addition & 0 deletions src/main/java/bio/overture/ego/model/enums/SqlFields.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public class SqlFields {
public static final String USER_ID = "user_id";
public static final String PROVIDERTYPE = "providertype";
public static final String PROVIDERSUBJECTID = "providersubjectid";
public static final String PROVIDERISSUERURI = "providerissueruri";
public static final String INITIALIZED = "initialized";
public static final String ERRORREDIRECTURI = "errorredirecturi";
public static final String SOURCE = "source";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ public OidcUser loadUser(OidcUserRequest oAuth2UserRequest) throws OAuth2Authent
OidcUser oidcUser = super.loadUser(oAuth2UserRequest);
try {
String provider = oAuth2UserRequest.getClientRegistration().getRegistrationId();
val idName = ProviderType.getIdAccessor(ProviderType.resolveProviderType(provider));
if (provider.equalsIgnoreCase(ProviderType.ORCID.toString())) {
val info = getOrcidUserInfo(oidcUser, oAuth2UserRequest);
val idName = ProviderType.getIdAccessor(ProviderType.resolveProviderType(provider));
return CustomOAuth2User.builder()
.oauth2User(new DefaultOAuth2User(oidcUser.getAuthorities(), info, idName))
.subjectId(info.get(idName).toString())
Expand Down
50 changes: 21 additions & 29 deletions src/main/java/bio/overture/ego/service/PassportService.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@
import lombok.val;
import org.apache.commons.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.LinkedMultiValueMap;
Expand All @@ -52,20 +53,12 @@ public class PassportService {

@Autowired private CacheUtil cacheUtil;

@Autowired private ClientRegistrationRepository clientRegistrationRepository;

private final String REQUESTED_TOKEN_TYPE = "urn:ga4gh:params:oauth:token-type:passport";
private final String SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
private final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";

@Value("${spring.security.oauth2.client.registration.passport.clientId}")
private String clientId;

@Value("${spring.security.oauth2.client.registration.passport.clientSecret}")
private String clientSecret;

@Value("${spring.security.oauth2.client.provider.passport.issuer-uri}")
private String passportIssuerUri;



@Autowired
public PassportService(
Expand All @@ -74,14 +67,14 @@ public PassportService(
this.visaPermissionService = visaPermissionService;
}

public List<VisaPermission> getPermissions(String authToken)
public List<VisaPermission> getPermissions(String authToken, String providerType)
throws JsonProcessingException, ParseException, JwkException {
// Validates passport auth token
isValidPassport(authToken);
isValidPassport(authToken, providerType);
// Parses passport JWT token
Passport parsedPassport = parsePassport(authToken);
// Fetches visas for parsed passport
List<PassportVisa> visas = getVisas(parsedPassport);
List<PassportVisa> visas = getVisas(parsedPassport, providerType);
// Fetches visa permissions for extracted visas
List<VisaPermission> visaPermissions = getVisaPermissions(visas);
// removes deduplicates from visaPermissions
Expand All @@ -90,22 +83,22 @@ public List<VisaPermission> getPermissions(String authToken)
}

// Validates passport token based on public key
private void isValidPassport(@NonNull String authToken)
throws ParseException, JwkException, JsonProcessingException {
private void isValidPassport(@NonNull String authToken, @NonNull String providerType)
throws JwkException {
DecodedJWT jwt = JWT.decode(authToken);
Jwk jwk = cacheUtil.getPassportBrokerPublicKey().get(jwt.getKeyId());
Jwk jwk = cacheUtil.getPassportBrokerPublicKey(providerType).get(jwt.getKeyId());
Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) jwk.getPublicKey(), null);
algorithm.verify(jwt);
}

// Extracts Visas from parsed passport object
private List<PassportVisa> getVisas(Passport passport) {
private List<PassportVisa> getVisas(Passport passport, @NonNull String providerType) {
List<PassportVisa> visas = new ArrayList<>();
passport.getGa4ghPassportV1().stream()
.forEach(
visaJwt -> {
try {
visaService.isValidVisa(visaJwt);
visaService.isValidVisa(visaJwt, providerType);
PassportVisa visa = visaService.parseVisa(visaJwt);
if (visa != null) {
visas.add(visa);
Expand Down Expand Up @@ -134,8 +127,8 @@ private List<VisaPermission> getVisaPermissions(List<PassportVisa> visas) {
return visaPermissions;
}

public Set<Scope> extractScopes(@NonNull String passportJwtToken) throws ParseException, JwkException, JsonProcessingException {
val resolvedPermissions = getPermissions(passportJwtToken);
public Set<Scope> extractScopes(@NonNull String passportJwtToken, @NonNull String providerType) throws ParseException, JwkException, JsonProcessingException {
val resolvedPermissions = getPermissions(passportJwtToken, providerType);
val output = mapToSet(resolvedPermissions, AbstractPermissionService::buildScope);
if (output.isEmpty()) {
output.add(Scope.defaultScope());
Expand All @@ -162,19 +155,18 @@ private List<VisaPermission> deDupeVisaPermissions(List<VisaPermission> visaPerm
return permissionsSet.stream().collect(Collectors.toList());
}

public String getPassportToken(String accessToken) {
public String getPassportToken(String providerId, String accessToken) {

if (accessToken == null || accessToken.isEmpty()) return null;

val params = passportTokenParams(accessToken);
val clientRegistration = clientRegistrationRepository.findByRegistrationId(providerId);

val uri = UriComponentsBuilder
.fromUriString(passportIssuerUri)
.path("/token")
.queryParams(params)
.fromUriString(clientRegistration.getProviderDetails().getTokenUri())
.queryParams(passportTokenParams(accessToken))
.toUriString();

val passportToken = getTemplate(clientId, clientSecret)
val passportToken = getTemplate(clientRegistration)
.exchange(uri,
HttpMethod.POST,
null,
Expand All @@ -186,7 +178,7 @@ public String getPassportToken(String accessToken) {
null;
}

private RestTemplate getTemplate(String clientId, String clientSecret) {
private RestTemplate getTemplate(ClientRegistration clientRegistration) {
RestTemplate restTemplate = new RestTemplate();
restTemplate
.getInterceptors()
Expand All @@ -195,7 +187,7 @@ private RestTemplate getTemplate(String clientId, String clientSecret) {
x.getHeaders()
.set(
HttpHeaders.AUTHORIZATION,
"Basic " + getBasicAuthHeader(clientId, clientSecret));
"Basic " + getBasicAuthHeader(clientRegistration.getClientId(), clientRegistration.getClientSecret()));
return z.execute(x, y);
});
return restTemplate;
Expand Down
16 changes: 8 additions & 8 deletions src/main/java/bio/overture/ego/service/TokenService.java
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,12 @@ public ApiKey getWithRelationships(@NonNull UUID id) {
}

public String generateUserToken(IDToken idToken) {
return generateUserToken(idToken, null);
return generateUserToken(idToken, null, null);
}

public String generateUserToken(IDToken idToken, String passportJwtToken) {
public String generateUserToken(IDToken idToken, String passportJwtToken, String providerType) {
val user = userService.getUserByToken(idToken);
return generateUserToken(user, passportJwtToken);
return generateUserToken(user, passportJwtToken, providerType);
}

public String updateUserToken(String accessToken) {
Expand Down Expand Up @@ -175,12 +175,12 @@ public String generateUserToken(User u) {
}

@SneakyThrows
public String generateUserToken(User u, String passportJwtToken) {
public String generateUserToken(User u, String passportJwtToken, String providerType) {

Set<String> scopes = extractExplicitScopes(u);

if (passportJwtToken != null){
Set<String> scopesFromVisas = extractExplicitScopes(passportJwtToken);
if (passportJwtToken != null && providerType != null){
Set<String> scopesFromVisas = extractExplicitScopes(passportJwtToken, providerType);
scopes = mergeScopes(scopes, scopesFromVisas);
}

Expand Down Expand Up @@ -601,8 +601,8 @@ private static Set<String> extractExplicitScopes(Application a) {
}

@SneakyThrows
private Set<String> extractExplicitScopes(String passportJwtToken){
return mapToSet(explicitScopes(passportService.extractScopes(passportJwtToken)), Scope::toString);
private Set<String> extractExplicitScopes(String passportJwtToken, String providerType){
return mapToSet(explicitScopes(passportService.extractScopes(passportJwtToken, providerType)), Scope::toString);
}

private Set<String> mergeScopes(Set<String> scopeSet, Set<String> scopeSetAdditional){
Expand Down
1 change: 1 addition & 0 deletions src/main/java/bio/overture/ego/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ public User createFromIDToken(IDToken idToken) {
.type(userDefaultsConfig.getDefaultUserType())
.providerType(idToken.getProviderType())
.providerSubjectId(idToken.getProviderSubjectId())
.providerIssuerUri(idToken.getProviderIssuerUri())
.build());
}

Expand Down
4 changes: 2 additions & 2 deletions src/main/java/bio/overture/ego/service/VisaService.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ public PassportVisa parseVisa(@NonNull String visaJwtToken) throws JsonProcessin
}

// Checks if the visa is a valid visa
public void isValidVisa(@NonNull String authToken) throws JwkException, JsonProcessingException {
public void isValidVisa(@NonNull String authToken, @NonNull String providerType) throws JwkException, JsonProcessingException {
DecodedJWT jwt = JWT.decode(authToken);
Jwk jwk = cacheUtil.getPassportBrokerPublicKey().get(jwt.getKeyId());
Jwk jwk = cacheUtil.getPassportBrokerPublicKey(providerType).get(jwt.getKeyId());
Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) jwk.getPublicKey(), null);
algorithm.verify(jwt);
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/bio/overture/ego/token/IDToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,6 @@ public class IDToken {
@JsonProperty("provider_subject_id")
@NonNull
String providerSubjectId;

private String providerIssuerUri;
}
15 changes: 9 additions & 6 deletions src/main/java/bio/overture/ego/utils/CacheUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@
import static org.springframework.http.HttpMethod.GET;

import com.auth0.jwk.Jwk;
import com.fasterxml.jackson.core.JsonProcessingException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import lombok.val;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
Expand All @@ -23,20 +24,22 @@
@Component
public class CacheUtil {

@Value("${broker.publicKey.url}")
private String brokerPublicKeyUrl;
@Autowired
private ClientRegistrationRepository clientRegistrationRepository;

private RestTemplate restTemplate;

@Cacheable("getPassportBrokerPublicKey")
public Map<String, Jwk> getPassportBrokerPublicKey() throws JsonProcessingException {
public Map<String, Jwk> getPassportBrokerPublicKey(String providerType) {
ResponseEntity<Map<String, List>> response;
Map<String, Jwk> jwkMap = new HashMap();

val clientRegistration = clientRegistrationRepository.findByRegistrationId(providerType);
try {
restTemplate = new RestTemplate();
response =
restTemplate.exchange(
brokerPublicKeyUrl,
clientRegistration.getProviderDetails().getJwkSetUri(),
GET,
null,
new ParameterizedTypeReference<Map<String, List>>() {});
Expand Down
5 changes: 2 additions & 3 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ spring:
- openid
- email
- profile

# Passport brokers must have ga4gh_passport_v1 scope
passport:
clientId: ego-client
clientSecret:
Expand Down Expand Up @@ -197,9 +199,6 @@ token:
private-key: MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDSU6oy48sJW6xzqzOSU1dAvUUeFKQSBHsCf7wGWUGpOxEczhtFiiyx4YUJtg+fyvwWxa4wO3GnQLBPIxBHY8JsnvjQN2lsTUoLqMB9nGpwF617uA/S2igm1u+cDpfi82kbi6SG1Sg30PM047R6oxTRGDLLkeMRF1gRaTBM0HfSL0j6ccU5KPgwYsFLE2We6jeR56iYJGC2KYLH4v8rcc2jRAdMbUntHMtUByF9BPSW7elQnyQH5Qzr/o0b59XLKwnJFn2Bp2yviC8cdyTDyhQGna0e+oESQR1j6u3Ux/mOmm3slRXscA8sH+pHmOEAtjYVf/ww36U8uZv+ctBCJyFVAgMBAAECggEBALrEeJqAFUfWFCkSmdUSFKT0bW/svFUTjXgGnZy1ncz9GpENpMH3lQDQVibteKpYwcom+Cr0XlQ66VUcudPrDjcOY7vhuMfnSh1YWLYyM4IeRHtcUxDVkFoM+vEFNHLf2zIOqqbgmboW3iDVIurT7iRO7KxAe/YtWJL9aVqMtBn7Lu7S7OvAU4ji5iLIBxjl82JYA+9lu/aQ6YGaoZuSO7bcU8Sivi+DKAahqN9XMKiB1XpC+PpaS/aec2S7xIlTdzoDGxEALRGlMe+xBEeQTBVJHBWrRIDPoHLTREeRC/9Pp+1Y4Dz8hd5Bi0n8/5r/q0liD+0vtmjsdU4E2QrktYECgYEA73qWvhCYHPMREAFtwz1mpp9ZhDCW6SF+njG7fBKcjz8OLcy15LXiTGc268ewtQqTMjPQlm1n2C6hGccGAIlMibQJo3KZHlTs125FUzDpTVgdlei6vU7M+gmfRSZed00J6jC04/qMR1tnV3HME3np7eRTKTA6Ts+zBwEvkbCetSkCgYEA4NY5iSBO1ybouIecDdD15uI2ItLPCBNMzu7IiK7IygIzuf+SyKyjhtFSR4vEi0gScOM7UMlwCMOVU10e4nMDknIWCDG9iFvmIEkGHGxgRrN5hX1Wrq74wF212lvvagH1IVWSHa8cVpMe+UwKu5Q1h4yzuYt6Q9wPQ7Qtn5emBE0CgYB2syispMUA9GnsqQii0Xhj9nAEWaEzhOqhtrzbTs5TIkoA4Yr3BkBY5oAOdjhcRBWZuJ0XMrtaKCKqCEAtW+CYEKkGXvMOWcHbNkkeZwv8zkQ73dNRqhFnjgVn3RDNyV20uteueK23YNLkQP+KV89fnuCpdcIw9joiqq/NYuIHoQKBgB5WaZ8KH/lCA8babYEjv/pubZWXUl4plISbja17wBYZ4/bl+F1hhhMr7Wk//743dF2NG7TT6W0VTvHXr9IoaMP65uQmKgfbNpsGn294ZClGEFClz+t0KpZyTpZvL0fjibr8u+GLfkxkP5qt2wjif7KRlrKjklTTva+KAVn2cW1FAoGBAMkX9ekIwhx/7uY6ndxKl8ZMDerjr6MhV0b08hHp3RxHbYVbcpN0UKspoYvZVgHwP18xlDij8yWRE2fapwgi4m82ZmYlg0qqJmyqIU9vBB3Jow903h1KPQrkmQEZxJ/4H8yrbgVf2HT+WUfjTFgaDZRl01bI3YkydCw91/Ub9HU6
public-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0lOqMuPLCVusc6szklNXQL1FHhSkEgR7An+8BllBqTsRHM4bRYosseGFCbYPn8r8FsWuMDtxp0CwTyMQR2PCbJ740DdpbE1KC6jAfZxqcBete7gP0tooJtbvnA6X4vNpG4ukhtUoN9DzNOO0eqMU0Rgyy5HjERdYEWkwTNB30i9I+nHFOSj4MGLBSxNlnuo3keeomCRgtimCx+L/K3HNo0QHTG1J7RzLVAchfQT0lu3pUJ8kB+UM6/6NG+fVyysJyRZ9gadsr4gvHHckw8oUBp2tHvqBEkEdY+rt1Mf5jppt7JUV7HAPLB/qR5jhALY2FX/8MN+lPLmb/nLQQichVQIDAQAB

broker:
publicKey:
url: https://login.elixir-czech.org/oidc/jwk

# Default values available for creation of entities
default:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE egouser ADD COLUMN providerissueruri VARCHAR(255);