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

add Keycloak auth service #830

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Bean
public SystemSecurity systemSecurity() {
return new SystemSecurity(scope.getSystem());
return new SystemSecurity(scope.getSystem(), introspectionUri);
}

@Bean
Expand All @@ -93,6 +93,7 @@ public StudySecurity studySecurity(@Autowired SystemSecurity systemSecurity) {
.studyPrefix(scope.getStudy().getPrefix())
.studySuffix(scope.getStudy().getSuffix())
.systemScope(scope.getSystem())
.introspectionUri(introspectionUri)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package bio.overture.song.server.security;

import lombok.Builder;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.oauth2.server.resource.introspection.BadOpaqueTokenException;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.io.IOException;
import java.util.List;

import static org.springframework.http.HttpStatus.Series.CLIENT_ERROR;
import static org.springframework.http.HttpStatus.Series.SERVER_ERROR;

@Slf4j
@Builder
public class KeycloakAuthorizationService {

private static final String UMA_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:uma-ticket";
private static final String UMA_AUDIENCE = "song";
private static final String UMA_RESPONSE_MODE = "permissions";

@NonNull private String introspectionUri;

public List<KeycloakPermission> fetchAuthorizationGrants(String accessToken){
// Add token to introspectionUri
val uriWithToken =
UriComponentsBuilder.fromHttpUrl(introspectionUri)
.build()
.toUri();

HttpEntity<MultiValueMap<String, String>> request =
new HttpEntity<>(getUmaParams(), getBearerAuthHeader(accessToken));

// Get response from Keycloak
val template = new RestTemplate();
template.setErrorHandler(new RestTemplateResponseErrorHandler());
val response =
template.postForEntity(
uriWithToken, request, KeycloakPermission[].class);

// Ensure response was OK
if ((response.getStatusCode() != HttpStatus.OK
&& response.getStatusCode() != HttpStatus.MULTI_STATUS
&& response.getStatusCode() != HttpStatus.UNAUTHORIZED)
|| !response.hasBody()) {
throw new OAuth2IntrospectionException("Bad Response from Keycloak Server");
}

val isValid = validateIntrospectResponse(response.getStatusCode());
if (!isValid) {
throw new BadOpaqueTokenException("ApiKey is revoked or expired.");
}

return List.of(response.getBody());
}

private MultiValueMap<String, String> getUmaParams(){
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("grant_type", UMA_GRANT_TYPE);
map.add("audience", UMA_AUDIENCE);
map.add("response_mode", UMA_RESPONSE_MODE);
return map;
}

private HttpHeaders getBearerAuthHeader(String token) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setBearerAuth(token);
return headers;
}

private boolean validateIntrospectResponse(HttpStatus status) {
if (status != HttpStatus.OK && status != HttpStatus.MULTI_STATUS) {
log.debug(
"Check Token response is unauthorized but does not list the error. Rejecting token.");
return false;
}
return true;
}

private static class RestTemplateResponseErrorHandler
implements ResponseErrorHandler {

@Override
public boolean hasError(ClientHttpResponse httpResponse)
throws IOException {

return (
httpResponse.getStatusCode().series() == CLIENT_ERROR
|| httpResponse.getStatusCode().series() == SERVER_ERROR);
}

@Override
public void handleError(ClientHttpResponse httpResponse)
throws IOException {

if (httpResponse.getStatusCode().series() == CLIENT_ERROR) {
// throw 401 HTTP error code
throw new BadCredentialsException(httpResponse.getStatusText());
} else {
// throw 500 HTTP error code
throw new OAuth2IntrospectionException(httpResponse.getStatusText());
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package bio.overture.song.server.security;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class KeycloakPermission {
private String rsid;
private String rsname;
private List<String> scopes;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,17 @@
*/
package bio.overture.song.server.security;

import static bio.overture.song.server.utils.Scopes.extractGrantedScopes;
import static bio.overture.song.server.utils.Scopes.extractGrantedScopesFromRpt;

import java.util.Set;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;

@Slf4j
@Value
Expand All @@ -34,10 +36,22 @@ public class StudySecurity {
@NonNull private final String studyPrefix;
@NonNull private final String studySuffix;
@NonNull private final String systemScope;
@NonNull private final String introspectionUri;

@Autowired
public KeycloakAuthorizationService keycloakAuthorizationService() {
return KeycloakAuthorizationService.builder()
.introspectionUri(introspectionUri)
.build();
}

public boolean authorize(@NonNull Authentication authentication, @NonNull final String studyId) {
log.info("Checking study-level authorization for studyId {}", studyId);
val grantedScopes = extractGrantedScopes(authentication);

val authGrants = keycloakAuthorizationService()
.fetchAuthorizationGrants(((JwtAuthenticationToken) authentication).getToken().getTokenValue());

val grantedScopes = extractGrantedScopesFromRpt(authGrants);
return verifyOneOfStudyScope(grantedScopes, studyId);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,38 @@
package bio.overture.song.server.security;

import static bio.overture.song.server.utils.Scopes.extractGrantedScopes;
import static bio.overture.song.server.utils.Scopes.extractGrantedScopesFromRpt;

import java.util.Set;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;

@Slf4j
@Value
public class SystemSecurity {

@NonNull private final String systemScope;
@NonNull private final String introspectionUri;

@Autowired
public KeycloakAuthorizationService keycloakAuthorizationService() {
return KeycloakAuthorizationService.builder()
.introspectionUri(introspectionUri)
.build();
}

public boolean authorize(@NonNull Authentication authentication) {
log.debug("Checking system-level authorization");
val grantedScopes = extractGrantedScopes(authentication);

val authGrants = keycloakAuthorizationService()
.fetchAuthorizationGrants(((JwtAuthenticationToken) authentication).getToken().getTokenValue());

val grantedScopes = extractGrantedScopesFromRpt(authGrants);
return verifyOneOfSystemScope(grantedScopes);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

import static lombok.AccessLevel.PRIVATE;

import bio.overture.song.server.security.KeycloakPermission;
import com.nimbusds.jose.shaded.json.JSONArray;
import com.nimbusds.jose.shaded.json.JSONObject;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import java.util.*;

import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
Expand All @@ -33,6 +33,22 @@ public static Set<String> extractGrantedScopes(Authentication authentication) {
return grantedScopes;
}

public static Set<String> extractGrantedScopesFromRpt(List<KeycloakPermission> permissionList) {
Set<String> grantedScopes = new HashSet();

permissionList
.stream()
.filter(perm -> perm.getScopes() != null)
.forEach(permission -> {
permission.getScopes().stream().forEach(scope -> {
val fullScope = permission.getRsname() + "." + scope;
grantedScopes.add(fullScope);
});
});

return grantedScopes;
}

public static long extractExpiry(Map<String, ?> map) {
Object exp = map.get(EXP);
if (exp instanceof Integer) {
Expand Down
7 changes: 5 additions & 2 deletions song-server/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,13 @@ spring:
oauth2:
resourceserver:
jwt:
public-key-location: http://localhost:8081/oauth/token/public_key
# EGO public key
# public-key-location: http://localhost:8081/oauth/token/public_key
# Keycloak JWT
jwk-set-uri: http://localhost/realms/myrealm/protocol/openid-connect/certs
auth:
server:
introspectionUri: http://localhost:8081/o/check_api_key
introspectionUri: http://localhost/realms/myrealm/protocol/openid-connect/token
clientID: id
clientSecret: secret
scope:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ public void test_authorize(Map<String, ?> map, boolean expected) {
val prefix = "PROGRAMDATA-";
val suffix = ".READ";
val scope = "DCC.READ";
val studySecurity = new StudySecurity(prefix, suffix, scope);
val introspectorUri = "http://localhost/realms/myrealm/protocol/openid-connect/token";
val studySecurity = new StudySecurity(prefix, suffix, scope, introspectorUri);
val authentication = new AccessTokenConverterWithExpiry().extractAuthentication(map);

assertEquals(expected, studySecurity.authorize(authentication, TEST_STUDY));
Expand Down