Skip to content

Commit

Permalink
Support for IDCS specific feature in OIDC config. (#1688)
Browse files Browse the repository at this point in the history
* Support for IDCS specific feature in OIDC config.
  • Loading branch information
tomas-langer authored May 5, 2020
1 parent 0e8f92a commit e8abee8
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2020 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2018, 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -69,6 +69,8 @@ public static void main(String[] args) throws IOException {
"https://idcs-tenant-id.identity.oracle.com"))
//.proxyHost("proxy.proxy.com")
.frontendUri("http://your.host:your.port")
// tell us it is IDCS, so we can modify the behavior
.serverType("idcs")
.build();

Security security = Security.builder()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# Copyright (c) 2018, 2020 Oracle and/or its affiliates. All rights reserved.
# Copyright (c) 2018, 2020 Oracle and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -38,6 +38,8 @@ security:
scope-audience: "http://localhost:7987/test-application"
proxy-host: "${ALIAS=security.properties.proxy-host}"
frontend-uri: "${ALIAS=security.properties.frontend-uri}"
# support for non-public signature JWK (and maybe other IDCS specific handling)
server-type: "idcs"
- idcs-role-mapper:
multitenant: false
oidc-config:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2018, 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -70,6 +70,16 @@ public static Builder builder() {
return new Builder();
}

/**
* Create Jwk keys from its JSON representation.
*
* @param json json with jwk keys
* @return keys set up from the provided json
*/
public static JwkKeys create(JsonObject json) {
return builder().json(json).build();
}

/**
* Get a JWK for defined key id if present.
*
Expand Down Expand Up @@ -147,6 +157,17 @@ public Builder resource(Resource resource) {
return this;
}

/**
* Load keys from JSON.
*
* @param json the JSON data
* @return updated builder instance
*/
public Builder json(JsonObject json) {
addKeys(json);
return this;
}

private void addKeys(JsonObject jsonObject) {
JsonArray keyArray = jsonObject.getJsonArray("keys");
keyArray.forEach(it -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -302,7 +302,17 @@ protected Builder() {
* @return updated builder instance
*/
public B config(Config config) {
config.get("oidc-config").as(OidcConfig::create).ifPresent(this::oidcConfig);
config.get("oidc-config").ifExists(it -> {
OidcConfig.Builder builder = OidcConfig.builder();
// we do not need JWT validation at all
builder.validateJwtWithJwk(false);
// this is an IDCS specific extension
builder.serverType("idcs");
builder.config(it);

oidcConfig(builder.build());
});

config.get("subject-types").asList(cfg -> cfg.asString().map(SubjectType::valueOf).get())
.ifPresent(list -> list.forEach(this::addSubjectType));
config.get("default-idcs-subject-type").asString().ifPresent(this::defaultIdcsSubjectType);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright (c) 2020 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.helidon.security.providers.oidc.common;

import java.net.URI;

import javax.json.JsonObject;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;

import io.helidon.common.Errors;
import io.helidon.security.jwt.jwk.JwkKeys;

/**
* Oracle IDCS specific implementations for {@code idcs} server type.
*/
class IdcsSupport {
// prevent instantiation
private IdcsSupport() {
}
// load signature jwk with a token
static JwkKeys signJwk(Client generalClient, WebTarget tokenEndpoint, Errors.Collector collector, URI signJwkUri) {
// need to get token to be able to request this endpoint
MultivaluedMap<String, String> formData = new MultivaluedHashMap<>();
formData.putSingle("grant_type", "client_credentials");
formData.putSingle("scope", "urn:opc:idm:__myscopes__");

JsonObject response = tokenEndpoint.request()
.accept(MediaType.APPLICATION_JSON_TYPE)
.post(Entity.form(formData), JsonObject.class);
String accessToken = response.getString("access_token");

// get the jwk from server
JsonObject jwkJson = generalClient.target(signJwkUri)
.request()
.header("Authorization", "Bearer " + accessToken)
.get(JsonObject.class);

return JwkKeys.create(jwkJson);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,11 @@
* <td>Maximal number of times we can redirect to an identity server. When the number is reached, no further redirects
* happen and the request finishes with an error (status {@code 401})</td>
* </tr>
* <tr>
* <td>server-type</td>
* <td>&nbsp;</td>
* <td>Type of identity server. Currently supported is {@code idcs} or not configured (for default).</td>
* </tr>
* </table>
*/
public final class OidcConfig {
Expand Down Expand Up @@ -325,32 +330,16 @@ private OidcConfig(Builder builder) {
this.realm = builder.realm;
this.redirectAttemptParam = builder.redirectAttemptParam;
this.maxRedirects = builder.maxRedirects;
this.appClient = builder.appClient;
this.tokenEndpoint = builder.tokenEndpoint;
this.generalClient = builder.generalClient;

if (null == builder.signJwk) {
this.signJwk = JwkKeys.builder().build();
} else {
this.signJwk = builder.signJwk;
}

ClientBuilder clientBuilder = ClientBuilder.newBuilder();

if (builder.proxyHost != null) {
clientBuilder.property(ClientProperties.PROXY_URI,
builder.proxyUri);
}

this.generalClient = clientBuilder.build();

HttpAuthenticationFeature basicAuth = HttpAuthenticationFeature.basicBuilder()
.credentials(builder.clientId, builder.clientSecret)
.build();

this.appClient = clientBuilder
.register(basicAuth)
.build();

this.tokenEndpoint = appClient.target(builder.tokenEndpointUri);

if (validateJwtWithJwk) {
this.introspectEndpoint = null;
} else {
Expand Down Expand Up @@ -688,6 +677,8 @@ public int maxRedirects() {
* A fluent API {@link io.helidon.common.Builder} to build instances of {@link OidcConfig}.
*/
public static class Builder implements io.helidon.common.Builder<OidcConfig> {
private static final String DEFAULT_SERVER_TYPE = "@default";

private String issuer;
private String audience;
private String baseScopes = DEFAULT_BASE_SCOPES;
Expand Down Expand Up @@ -736,9 +727,24 @@ public static class Builder implements io.helidon.common.Builder<OidcConfig> {
private String redirectAttemptParam = DEFAULT_ATTEMPT_PARAM;
private int maxRedirects = DEFAULT_MAX_REDIRECTS;
private boolean cookieSameSiteDefault = true;
private String serverType;
private Client generalClient;
private WebTarget tokenEndpoint;
private Client appClient;

@Override
public OidcConfig build() {
if (null != serverType) {
// explicit server type
if (!"idcs".equals(serverType)) {
LOGGER.warning("OIDC server-type is configured to " + serverType + ", currently only \"idcs\", and"
+ " \"" + DEFAULT_SERVER_TYPE + "\" are supported");
serverType = DEFAULT_SERVER_TYPE;
}
} else {
serverType = DEFAULT_SERVER_TYPE;
}

if ((null == proxyUri) && (null != proxyHost)) {
this.proxyUri = proxyProtocol
+ "://"
Expand Down Expand Up @@ -776,26 +782,6 @@ public OidcConfig build() {
"authorization_endpoint",
"/oauth2/v1/authorize");

if (validateJwtWithJwk) {
if (null == signJwk) {
// not configured - use default location
URI jwkUri = getOidcEndpoint(collector,
null,
"jwks_uri",
null);
if (null != jwkUri) {
this.signJwk = JwkKeys.builder()
.resource(Resource.create(jwkUri))
.build();
}
}
} else {
this.introspectUri = getOidcEndpoint(collector,
introspectUri,
"introspection_endpoint",
"/oauth2/v1/introspect");
}

if ((null == issuer) && (null != oidcMetadata)) {
this.issuer = oidcMetadata.getString("issuer");
}
Expand Down Expand Up @@ -823,6 +809,49 @@ public OidcConfig build() {
}
}

ClientBuilder clientBuilder = ClientBuilder.newBuilder();

if (proxyHost != null) {
clientBuilder.property(ClientProperties.PROXY_URI, proxyUri);
}

this.generalClient = clientBuilder.build();


HttpAuthenticationFeature basicAuth = HttpAuthenticationFeature.basicBuilder()
.credentials(clientId, clientSecret)
.build();

appClient = clientBuilder
.register(basicAuth)
.build();

tokenEndpoint = appClient.target(tokenEndpointUri);

if (validateJwtWithJwk) {
if (null == signJwk) {
// not configured - use default location
URI jwkUri = getOidcEndpoint(collector,
null,
"jwks_uri",
null);
if (null != jwkUri) {
if ("idcs".equals(serverType)) {
this.signJwk = IdcsSupport.signJwk(generalClient, tokenEndpoint, collector, jwkUri);
} else {
this.signJwk = JwkKeys.builder()
.resource(Resource.create(jwkUri))
.build();
}
}
}
} else {
this.introspectUri = getOidcEndpoint(collector,
introspectUri,
"introspection_endpoint",
"/oauth2/v1/introspect");
}

return new OidcConfig(this);
}

Expand Down Expand Up @@ -931,6 +960,10 @@ public Builder config(Config config) {
config.get("redirect-attempt-param").asString().ifPresent(this::redirectAttemptParam);
config.get("max-redirects").asInt().ifPresent(this::maxRedirects);

// type of the identity server
// now uses hardcoded switch - should change to service loader eventually
config.get("server-type").asString().ifPresent(this::serverType);

return this;
}

Expand Down Expand Up @@ -1400,5 +1433,18 @@ public Builder maxRedirects(int maxRedirects) {
this.maxRedirects = maxRedirects;
return this;
}

/**
* Configure one of the supported types of identity servers.
*
* If the type does not have an explicit mapping, a warning is logged and the default implementation is used.
*
* @param type Type of identity server. Currently supported is {@code idcs} or not configured (for default).
* @return updated builder instance
*/
public Builder serverType(String type) {
this.serverType = type;
return this;
}
}
}

0 comments on commit e8abee8

Please sign in to comment.