Skip to content

add Keycloak auth service #836

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

Merged
merged 7 commits into from
Jan 17, 2024
Merged
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ package:
# Start ego, score, and object-storage.
start-deps: _setup package
@echo $(YELLOW)$(INFO_HEADER) "Starting dependencies: ego, score and object-storage" $(END)
@$(DC_UP_CMD) ego-api score-server object-storage dcc-id-server
@$(DC_UP_CMD) ego-api score-server object-storage

# Start song-server and all dependencies. Affected by DEMO_MODE
start-song-server: _setup package start-deps _setup-object-storage
Expand Down
48 changes: 5 additions & 43 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
version: '3.7'
services:
ego-api:
image: "overture/ego:3.4.0"
image: "overture/ego:5.3.0"
environment:
SERVER_PORT: 8080
SPRING_DATASOURCE_URL: jdbc:postgresql://ego-postgres:5432/ego?stringtype=unspecified
SPRING_DATASOURCE_USERNAME: postgres
SPRING_DATASOURCE_PASSWORD: password
SPRING_FLYWAY_ENABLED: "true"
SPRING_FLYWAY_LOCATIONS: "classpath:flyway/sql,classpath:db/migration"
SPRING_PROFILES: demo, auth
SPRING_PROFILES_ACTIVE: demo, auth
JWT_DURATIONMS: 300000 # expire tokens in 5 min for local testing
expose:
- "8080"
Expand Down Expand Up @@ -43,7 +43,7 @@ services:
ports:
- "8085:9000"
score-server:
image: overture/score-server:5.1.0
image: overture/score-server:5.10.0
user: "$MY_UID:$MY_GID"
environment:
SPRING_PROFILES_ACTIVE: amazon,collaboratory,prod,secure,jwt
Expand Down Expand Up @@ -86,7 +86,7 @@ services:
- "./docker/scratch/score-server-logs:/score-server/logs"

score-client:
image: overture/score:5.0.0
image: overture/score:5.10.0
environment:
ACCESSTOKEN: f69b726d-d40f-4261-b105-1ec7e6bf04d5
METADATA_URL: http://song-server:8080
Expand Down Expand Up @@ -151,6 +151,7 @@ services:
AUTH_SERVER_CLIENTID: song
AUTH_SERVER_TOKENNAME: apiKey
AUTH_SERVER_CLIENTSECRET: songsecret
AUTH_SERVER_PROVIDER: ego
AUTH_SERVER_SCOPE_STUDY_PREFIX: song.
AUTH_SERVER_SCOPE_STUDY_SUFFIX: .WRITE
AUTH_SERVER_SCOPE_SYSTEM: song.WRITE
Expand All @@ -165,15 +166,6 @@ services:
SPRING_DATASOURCE_URL: jdbc:postgresql://song-db/song?stringtype=unspecified
SPRING_FLYWAY_ENABLED: "true"
SPRING_FLYWAY_LOCATIONS: "classpath:db/migration"
ID_USELOCAL: "false"
ID_FEDERATED_AUTH_BEARER_TOKEN: f69b726d-d40f-4261-b105-1ec7e6bf04d5
ID_FEDERATED_URITEMPLATE_DONOR: http://dcc-id-server:8080/donor/id?submittedProjectId={studyId}&submittedDonorId={submitterId}&create=true
ID_FEDERATED_URITEMPLATE_SPECIMEN: http://dcc-id-server:8080/specimen/id?submittedProjectId={studyId}&submittedSpecimenId={submitterId}&create=true
ID_FEDERATED_URITEMPLATE_SAMPLE: http://dcc-id-server:8080/sample/id?submittedProjectId={studyId}&submittedSampleId={submitterId}&create=true
ID_FEDERATED_URITEMPLATE_FILE: http://dcc-id-server:8080/object/id?analysisId={analysisId}&fileName={fileName}
ID_FEDERATED_URITEMPLATE_ANALYSIS_EXISTENCE: http://dcc-id-server:8080/analysis/id?submittedAnalysisId={analysisId}&create=false
ID_FEDERATED_URITEMPLATE_ANALYSIS_GENERATE: http://dcc-id-server:8080/analysis/unique
ID_FEDERATED_URITEMPLATE_ANALYSIS_SAVE: http://dcc-id-server:8080/analysis/id?submittedAnalysisId={analysisId}&create=true
JAVA_TOOL_OPTIONS: -agentlib:jdwp=transport=dt_socket,address=*:5006,server=y,suspend=n
restart: always
ports:
Expand All @@ -183,39 +175,9 @@ services:
- song-db
- ego-api
- score-server
- dcc-id-server
volumes:
- "./docker/scratch/song-server-logs:/song-server/logs"
user: song
dcc-id-db:
image: icgcdcc/dcc-id-db:3908f82
environment:
POSTGRES_DB: dcc_identifier
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
dcc-id-server:
image: icgcdcc/dcc-id-server:3908f82
environment:
AUTH_SERVER_URL: http://ego-api:8080/o/check_token/
AUTH_SERVER_CLIENTID: dcc-id
AUTH_SERVER_CLIENTSECRET: dccidsecret
AUTH_SERVER_PREFIX: id
AUTH_SERVER_SUFFIX: WRITE
AUTH_CONNECTION_MAXRETRIES: 5
AUTH_CONNECTION_INITIALBACKOFF: 15000
AUTH_CONNECTION_MULTIPLIER: 2.0
SPRING_DATASOURCE_URL: jdbc:postgresql://dcc-id-db/dcc_identifier
SPRING_DATASOURCE_USERNAME: postgres
SPRING_DATASOURCE_PASSWORD: password
SPRING_PROFILES_ACTIVE: secure
restart: always
depends_on:
- dcc-id-db
- ego-api
expose:
- "8080"
ports:
- "8086:8080"

volumes:
object-storage-data: {}
4 changes: 3 additions & 1 deletion docker/ego-init/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,8 @@ ALTER TABLE public.userpermission OWNER TO postgres;
--

COPY public.egoapplication (name, clientid, clientsecret, redirecturi, description, status, id, type) FROM stdin;
song-to-score songToScore songToScoreSecret http://example.com song-to-score APPROVED 77f1ef78-7495-4b4a-982a-6b9532dc69fb CLIENT
song song songsecret http://example.com song-to-score APPROVED 77f1ef78-7495-4b4a-982a-6b9532dc69fb CLIENT
score score scoresecret http://example.com score APPROVED 8b573f1b-fbf8-413b-af5d-c17fd75aa00e CLIENT
\.


Expand Down Expand Up @@ -346,6 +347,7 @@ COPY public.flyway_schema_history (installed_rank, version, description, type, s

COPY public.groupapplication (group_id, application_id) FROM stdin;
f2885e96-f74e-4f7a-b935-fb48b18e761d 77f1ef78-7495-4b4a-982a-6b9532dc69fb
f2885e96-f74e-4f7a-b935-fb48b18e761d 8b573f1b-fbf8-413b-af5d-c17fd75aa00e
\.


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

import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

@Configuration
@Getter
public class KeycloakConfig {

@Value("${auth.server.clientID}")
private String uma_audience;

@Value("${auth.server.keycloak.host}")
private String host;

@Value("${auth.server.keycloak.realm}")
private String realm;

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

public URI permissionUrl(){
return UriComponentsBuilder.fromHttpUrl(host)
.pathSegment("realms", realm, "protocol/openid-connect/token")
.build()
.toUri();
}

public 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/
package bio.overture.song.server.config;

import bio.overture.song.server.security.EgoApiKeyIntrospector;
import bio.overture.song.server.security.ApiKeyIntrospector;
import bio.overture.song.server.security.StudySecurity;
import bio.overture.song.server.security.SystemSecurity;
import lombok.Getter;
Expand Down Expand Up @@ -67,38 +67,44 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
private String introspectionUri;
private String clientId;
private String clientSecret;
private String provider;
private String tokenName;

private final ScopeConfig scope = new ScopeConfig();

@Bean
public SystemSecurity systemSecurity() {
return new SystemSecurity(scope.getSystem());
return SystemSecurity.builder()
.systemScope(scope.getSystem())
.provider(provider)
.build();
}

@Bean
public AuthenticationManagerResolver<HttpServletRequest> tokenAuthenticationManagerResolver() {

// Auth Managers for JWT and for ApiKeys. JWT uses the default auth provider,
// but OpaqueTokens are handled by the custom EgoApiKeyIntrospector
// but OpaqueTokens are handled by the custom ApiKeyIntrospector
AuthenticationManager jwt = new ProviderManager(new JwtAuthenticationProvider(jwtDecoder));
AuthenticationManager opaqueToken =
new ProviderManager(new OpaqueTokenAuthenticationProvider(new EgoApiKeyIntrospector(introspectionUri, clientId, clientSecret)));
new ProviderManager(new OpaqueTokenAuthenticationProvider(new ApiKeyIntrospector(introspectionUri, clientId, clientSecret, tokenName)));

return (request) -> useJwt(request) ? jwt : opaqueToken;
}

@Bean
public StudySecurity studySecurity(@Autowired SystemSecurity systemSecurity) {
public StudySecurity studySecurity() {
return StudySecurity.builder()
.studyPrefix(scope.getStudy().getPrefix())
.studySuffix(scope.getStudy().getSuffix())
.systemScope(scope.getSystem())
.provider(provider)
.build();
}

@Bean
public OpaqueTokenIntrospector introspector() {
return new EgoApiKeyIntrospector(introspectionUri, clientId, clientSecret);
return new ApiKeyIntrospector(introspectionUri, clientId, clientSecret, tokenName);
}

@Override
Expand Down Expand Up @@ -158,7 +164,7 @@ private boolean useJwt(HttpServletRequest request) {
String token = authorizationHeaderValue.substring(7);
try {
UUID.fromString(token);
// able to parse as UUID, so this token matches our EgoApiKey format
// able to parse as UUID, so this token matches our ApiKey format
return false;
} catch (IllegalArgumentException e) {
// unable to parse as UUID, use our JWT resolvers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
@Data
@AllArgsConstructor
@NoArgsConstructor
public class EgoApiKeyIntrospectResponse {
public class ApiKeyIntrospectResponse {
private long exp;
private String user_id;
public List<String> scope;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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.core.GrantedAuthority;
Expand All @@ -17,6 +18,8 @@
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
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;
Expand All @@ -29,34 +32,39 @@

@Slf4j
@AllArgsConstructor
public class EgoApiKeyIntrospector implements OpaqueTokenIntrospector {
public class ApiKeyIntrospector implements OpaqueTokenIntrospector {

private String introspectionUri;
private String clientId;
private String clientSecret;
private String tokenName;

@Override
public OAuth2AuthenticatedPrincipal introspect(String token) {

// Add token to body
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add(tokenName, token);

// Add token to introspectionUri
val uriWithToken =
UriComponentsBuilder.fromHttpUrl(introspectionUri)
.queryParam("apiKey", token)
.build()
.toUri();
UriComponentsBuilder.fromHttpUrl(introspectionUri)
.build()
.toUri();

// Get response from Ego
val template = new RestTemplate();
template.setErrorHandler(new RestTemplateResponseErrorHandler());
val response =
template.postForEntity(
uriWithToken, new HttpEntity<Void>(null, getBasicAuthHeader()), JsonNode.class);
uriWithToken, new HttpEntity<>(formData, getBasicAuthHeader()), JsonNode.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 Ego Server");
throw new OAuth2IntrospectionException("Bad Response from Auth Server");
}

val responseBody = response.getBody();
Expand All @@ -73,6 +81,7 @@ public OAuth2AuthenticatedPrincipal introspect(String token) {
private HttpHeaders getBasicAuthHeader() {
val headers = new HttpHeaders();
headers.setBasicAuth(clientId, clientSecret);
headers.set(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE);
return headers;
}

Expand All @@ -86,15 +95,26 @@ private boolean validateIntrospectResponse(HttpStatus status, JsonNode response)
"Check Token response is unauthorized but does not list the error. Rejecting token.");
return false;
}
/* TODO: joneubank 2022-06-10 this should be checking for expiry and active=true instead of just the presence of
an error field, but at the moment the Ego check_api_token endpoint either returns 401+error, or it is active
(Ego version 5.3.0)
*/
if(response.has("exp") && response.get("exp").asLong() == 0){
log.debug("Token is expired. Rejecting token.");
return false;
}

if(response.has("revoked") && response.get("revoked").asBoolean() == true){
log.debug("Token is revoked. Rejecting token.");
return false;
}

if(response.has("valid") && response.get("valid").asBoolean() == false){
log.debug("Check Token response 'valid' field is false. Rejecting token.");
return false;
}

return true;
}

private OAuth2AuthenticatedPrincipal convertResponseToPrincipal(JsonNode responseJson) {
val response = JsonUtils.convertValue(responseJson, EgoApiKeyIntrospectResponse.class);
val response = JsonUtils.convertValue(responseJson, ApiKeyIntrospectResponse.class);

Collection<GrantedAuthority> authorities = new ArrayList();
Map<String, Object> claims = new HashMap<>();
Expand Down
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;
}
Loading