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

Support for IDCS specific feature in OIDC config. #1688

Merged
merged 2 commits into from
May 5, 2020
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
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;
}
}
}