mirror of https://github.com/apache/nifi.git
NIFI-4890 Refactor OIDC with support for Refresh Tokens (#7013)
* NIFI-4890 Refactored OIDC with support for Refresh Tokens - Implemented OIDC Authorization Code Grant Flow using Spring Security Filters - Implemented OIDC RP-Initiated Logout 1.0 - Implemented OAuth2 Token Revocation RFC 7009 for Refresh Tokens - Added OIDC Bearer Token Refresh Filter for updating application Bearer Tokens from Refresh Token exchanges - Added configurable Token Refresh Window to application properties - Removed original implementation and supporting classes * NIFI-4890 Set Bearer Token expiration based on Access Token * NIFI-4890 Corrected spelling and naming issues based on feedback This closes #7013
This commit is contained in:
parent
623bcfd500
commit
26400fcbe9
|
@ -195,6 +195,7 @@ public class NiFiProperties extends ApplicationProperties {
|
|||
public static final String SECURITY_USER_OIDC_CLAIM_IDENTIFYING_USER = "nifi.security.user.oidc.claim.identifying.user";
|
||||
public static final String NIFI_SECURITY_USER_OIDC_CLAIM_GROUPS = "nifi.security.user.oidc.claim.groups";
|
||||
public static final String SECURITY_USER_OIDC_FALLBACK_CLAIMS_IDENTIFYING_USER = "nifi.security.user.oidc.fallback.claims.identifying.user";
|
||||
public static final String SECURITY_USER_OIDC_TOKEN_REFRESH_WINDOW = "nifi.security.user.oidc.token.refresh.window";
|
||||
|
||||
// apache knox
|
||||
public static final String SECURITY_USER_KNOX_URL = "nifi.security.user.knox.url";
|
||||
|
@ -385,6 +386,7 @@ public class NiFiProperties extends ApplicationProperties {
|
|||
public static final String DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT = "5 secs";
|
||||
public static final String DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT = "5 secs";
|
||||
public static final String DEFAULT_SECURITY_USER_OIDC_TRUSTSTORE_STRATEGY = "JDK";
|
||||
private static final String DEFAULT_SECURITY_USER_OIDC_TOKEN_REFRESH_WINDOW = "60 secs";
|
||||
public static final String DEFAULT_SECURITY_USER_SAML_METADATA_SIGNING_ENABLED = "false";
|
||||
public static final String DEFAULT_SECURITY_USER_SAML_REQUEST_SIGNING_ENABLED = "false";
|
||||
public static final String DEFAULT_SECURITY_USER_SAML_WANT_ASSERTIONS_SIGNED = "true";
|
||||
|
@ -1180,6 +1182,10 @@ public class NiFiProperties extends ApplicationProperties {
|
|||
return getProperty(SECURITY_USER_OIDC_TRUSTSTORE_STRATEGY, DEFAULT_SECURITY_USER_OIDC_TRUSTSTORE_STRATEGY);
|
||||
}
|
||||
|
||||
public String getOidcTokenRefreshWindow() {
|
||||
return getProperty(SECURITY_USER_OIDC_TOKEN_REFRESH_WINDOW, DEFAULT_SECURITY_USER_OIDC_TOKEN_REFRESH_WINDOW);
|
||||
}
|
||||
|
||||
public boolean shouldSendServerVersion() {
|
||||
return Boolean.parseBoolean(getProperty(WEB_SHOULD_SEND_SERVER_VERSION, DEFAULT_WEB_SHOULD_SEND_SERVER_VERSION));
|
||||
}
|
||||
|
|
|
@ -258,7 +258,7 @@ properties can be specified.
|
|||
NOTE: It is important when enabling HTTPS that the `nifi.web.http.port` property be unset. NiFi only supports running on HTTP *or* HTTPS, not both simultaneously.
|
||||
|
||||
NiFi's web server will REQUIRE certificate based client authentication for users accessing the User Interface when not configured with an alternative
|
||||
authentication mechanism which would require one way SSL (for instance LDAP, OpenId Connect, etc). Enabling an alternative authentication mechanism will
|
||||
authentication mechanism which would require one way SSL (for instance LDAP, OpenID Connect, etc). Enabling an alternative authentication mechanism will
|
||||
configure the web server to WANT certificate base client authentication. This will allow it to support users with certificates and those without that
|
||||
may be logging in with credentials. See <<user_authentication>> for more details.
|
||||
|
||||
|
@ -315,9 +315,9 @@ The semantics match the use of the following Jetty APIs:
|
|||
[[user_authentication]]
|
||||
== User Authentication
|
||||
|
||||
NiFi supports user authentication via client certificates, via username/password, via Apache Knox, or via link:http://openid.net/connect[OpenId Connect^].
|
||||
NiFi supports user authentication using a number of configurable protocols and strategies.
|
||||
|
||||
Username/password authentication is performed by a 'Login Identity Provider'. The Login Identity Provider is a pluggable mechanism for
|
||||
Username and password authentication is performed by a 'Login Identity Provider'. The Login Identity Provider is a pluggable mechanism for
|
||||
authenticating users via their username/password. Which Login Identity Provider to use is configured in the _nifi.properties_ file.
|
||||
Currently NiFi offers username/password with Login Identity Providers options for <<single_user_identity_provider>>, <<ldap_login_identity_provider>> and <<kerberos_login_identity_provider>>.
|
||||
|
||||
|
@ -326,14 +326,14 @@ The `nifi.login.identity.provider.configuration.file` property specifies the con
|
|||
The `nifi.security.user.login.identity.provider` property indicates which of the configured Login Identity Provider should be
|
||||
used. The default value of this property is `single-user-provider` supporting authentication with a generated username and password.
|
||||
|
||||
During OpenId Connect authentication, NiFi will redirect users to login with the Provider before returning to NiFi. NiFi will then
|
||||
call the Provider to obtain the user identity.
|
||||
For Single sign-on authentication, NiFi will redirect users to the Identity Provider before returning to NiFi. NiFi will then
|
||||
process responses and convert attributes to application token information.
|
||||
|
||||
During Apache Knox authentication, NiFi will redirect users to login with Apache Knox before returning to NiFi. NiFi will verify the Apache Knox
|
||||
token during authentication.
|
||||
|
||||
NOTE: NiFi can only be configured for username/password, OpenId Connect, or Apache Knox at a given time. It does not support running each of
|
||||
these concurrently. NiFi will require client certificates for authenticating users over HTTPS if none of these are configured.
|
||||
NOTE: NiFi cannot be configured for multiple authentication strategies simultaneously.
|
||||
NiFi will require client certificates for authenticating users over HTTPS if no other strategies have been configured.
|
||||
|
||||
A user cannot anonymously authenticate with a secured instance of NiFi unless `nifi.security.allow.anonymous.authentication` is set to `true`.
|
||||
If this is the case, NiFi must also be configured with an Authorizer that supports authorizing an anonymous user. Currently, NiFi does not ship
|
||||
|
@ -490,31 +490,81 @@ See also <<kerberos_service>> to allow single sign-on access via client Kerberos
|
|||
NOTE: For changes to _nifi.properties_ and _login-identity-providers.xml_ to take effect, NiFi needs to be restarted. If NiFi is clustered, configuration files must be the same on all nodes.
|
||||
|
||||
[[openid_connect]]
|
||||
=== OpenId Connect
|
||||
=== OpenID Connect
|
||||
|
||||
To enable authentication via OpenId Connect the following properties must be configured in _nifi.properties_.
|
||||
OpenID Connect integration provides single sign-on using a specified Authorization Server.
|
||||
The implementation supports the Authorization Code Grant Type as described in
|
||||
link:https://www.rfc-editor.org/rfc/rfc6749#section-4.1[RFC 6749 Section 4.1^] and
|
||||
link:https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowSteps[OpenID Connect Core Section 3.1.1^].
|
||||
|
||||
After successful authentication with the Authorization Server, NiFi generates an application Bearer Token with an
|
||||
expiration based on the OAuth2 Access Token expiration. NiFi stores authorized tokens using the local State
|
||||
Provider and encrypts serialized information using the application Sensitive Properties Key.
|
||||
|
||||
The implementation enables
|
||||
link:https://openid.net/specs/openid-connect-rpinitiated-1_0.html[OpenID Connect RP-Initiated Logout 1.0^] when the
|
||||
Authorization Server includes an `end_session_endpoint` element in the OpenID Discovery configuration.
|
||||
|
||||
OpenID Connect integration supports using Refresh Tokens as described in
|
||||
link:https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens[OpenID Connect Core Section 12].
|
||||
NiFi tracks the expiration of the application Bearer Token and uses the stored Refresh Token to renew
|
||||
access prior to Bearer Token expiration, based on the configured token refresh window. NiFi does not require OpenID
|
||||
Connect Providers to support Refresh Tokens. When an OpenID Connect Provider does not return a Refresh Token, NiFi
|
||||
requires the user to initiate a new session when the application Bearer Token expires.
|
||||
|
||||
The Refresh Token implementation allows the NiFi session to continue as long as the Refresh Token is valid and the
|
||||
user agent presents a valid Bearer Token. The default value for the token refresh window is 60 seconds. For an Access
|
||||
Token with an expiration of one hour, NiFi will attempt to renew access using the Refresh Token when receiving an HTTP
|
||||
request 59 minutes after authenticating the Access Token. Revoked Refresh Tokens or expired application Bearer Tokens
|
||||
result in standard session timeout behavior, requiring the user to initiate a new session.
|
||||
|
||||
The OpenID Connect implementation supports OAuth 2.0 Token Revocation as defined in
|
||||
link:https://www.rfc-editor.org/rfc/rfc7009[RFC 7009^]. OpenID Connect Discovery configuration must include a
|
||||
`revocation_endpoint` element that supports RFC 7009 standards. The application sends revocation requests for Refresh
|
||||
Tokens when the authenticated Resource Owner initiates the logout process.
|
||||
|
||||
The implementation includes a scheduled process for removing and revoking expired Refresh Tokens when the corresponding
|
||||
Access Token has expired, indicating that the Resource Owner has terminated the application session. Scheduled session
|
||||
termination occurs when the user closes the browser without initiating the logout process. The scheduled process avoids
|
||||
extended storage of Refresh Tokens for users who are no longer interacting with the application.
|
||||
|
||||
OpenID Connect integration supports the following settings in _nifi.properties_.
|
||||
|
||||
[options="header"]
|
||||
|==================================================================================================================================================
|
||||
| Property Name | Description
|
||||
|`nifi.security.user.oidc.discovery.url` | The discovery URL for the desired OpenId Connect Provider (link:http://openid.net/specs/openid-connect-discovery-1_0.html[http://openid.net/specs/openid-connect-discovery-1_0.html^]).
|
||||
|`nifi.security.user.oidc.connect.timeout` | Connect timeout when communicating with the OpenId Connect Provider. The default value is `5 secs`.
|
||||
|`nifi.security.user.oidc.read.timeout` | Read timeout when communicating with the OpenId Connect Provider. The default value is `5 secs`.
|
||||
|`nifi.security.user.oidc.client.id` | The client id for NiFi after registration with the OpenId Connect Provider.
|
||||
|`nifi.security.user.oidc.client.secret` | The client secret for NiFi after registration with the OpenId Connect Provider.
|
||||
|`nifi.security.user.oidc.preferred.jwsalgorithm` | The preferred algorithm for validating identity tokens. If this value is blank, it will default to `RS256` which is required to be supported
|
||||
by the OpenId Connect Provider according to the specification. If this value is `HS256`, `HS384`, or `HS512`, NiFi will attempt to validate HMAC protected tokens using the specified client secret.
|
||||
| Property Name | Description
|
||||
|`nifi.security.user.oidc.discovery.url` | The link:http://openid.net/specs/openid-connect-discovery-1_0.html[Discovery Configuration URL^] for the OpenID Connect Provider
|
||||
|`nifi.security.user.oidc.connect.timeout` | Socket Connect timeout when communicating with the OpenID Connect Provider. The default value is `5 secs`
|
||||
|`nifi.security.user.oidc.read.timeout` | Socket Read timeout when communicating with the OpenID Connect Provider. The default value is `5 secs`
|
||||
|`nifi.security.user.oidc.client.id` | The Client ID for NiFi registered with the OpenID Connect Provider
|
||||
|`nifi.security.user.oidc.client.secret` | The Client Secret for NiFi registered with the OpenID Connect Provider
|
||||
|`nifi.security.user.oidc.preferred.jwsalgorithm` | The preferred algorithm for validating identity tokens. If this value is blank, it will default to `RS256` which is required to be supported
|
||||
by the OpenID Connect Provider according to the specification. If this value is `HS256`, `HS384`, or `HS512`, NiFi will attempt to validate HMAC protected tokens using the specified client secret.
|
||||
If this value is `none`, NiFi will attempt to validate unsecured/plain tokens. Other values for this algorithm will attempt to parse as an RSA or EC algorithm to be used in conjunction with the
|
||||
JSON Web Key (JWK) provided through the jwks_uri in the metadata found at the discovery URL.
|
||||
|`nifi.security.user.oidc.additional.scopes` | Comma separated scopes that are sent to OpenId Connect Provider in addition to `openid` and `email`.
|
||||
|`nifi.security.user.oidc.claim.identifying.user` | Claim that identifies the user to be logged in; default is `email`. May need to be requested via the `nifi.security.user.oidc.additional.scopes` before usage.
|
||||
|`nifi.security.user.oidc.fallback.claims.identifying.user` | Comma separated possible fallback claims used to identify the user in case `nifi.security.user.oidc.claim.identifying.user` claim is not present for the login user.
|
||||
|`nifi.security.user.oidc.claim.groups` | Name of the ID token claim that contains an array of group names of which the
|
||||
JSON Web Key (JWK) provided through the jwks_uri in the metadata found at the discovery URL
|
||||
|`nifi.security.user.oidc.additional.scopes` | Comma separated scopes that are sent to OpenID Connect Provider in addition to `openid` and `email`
|
||||
|`nifi.security.user.oidc.claim.identifying.user` | Claim that identifies the authenticated user. The default value is `email`. Claim names may need to be requested using the `nifi.security.user.oidc.additional.scopes` property
|
||||
|`nifi.security.user.oidc.fallback.claims.identifying.user` | Comma-separated list of possible fallback claims used to identify the user when the `nifi.security.user.oidc.claim.identifying.user` claim is not found.
|
||||
|`nifi.security.user.oidc.claim.groups` | Name of the ID token claim that contains an array of group names of which the
|
||||
user is a member. Application groups must be supplied from a User Group Provider with matching names in order for the
|
||||
authorization process to use ID token claim groups. The default value is `groups`.
|
||||
|`nifi.security.user.oidc.truststore.strategy` | If value is `NIFI`, use the NiFi truststore when connecting to the OIDC service, otherwise if value is `JDK` use Java's default `cacerts` truststore. The default value is `JDK`.
|
||||
|`nifi.security.user.oidc.truststore.strategy` | HTTPS Certificate Trust Store Strategy defines the source of certificate authorities that NiFi uses when communicating with the OpenID Connect Provider.
|
||||
The value of `JDK` uses the Java platform default configuration stored in `cacerts` under the Java Home directory.
|
||||
The value of `NIFI` enables using the trust store configured in the `nifi.security.truststore` property. The default value is `JDK`
|
||||
|`nifi.security.user.oidc.token.refresh.window` | The Token Refresh Window specifies the amount of time before the NiFi authorization session expires when the application will attempt to renew access using a cached Refresh Token. The default is `60 secs`
|
||||
|==================================================================================================================================================
|
||||
|
||||
==== OpenID Connect REST Resources
|
||||
|
||||
OpenID Connect authentication enables the following REST resources for integration with an OpenID Connect 1.0 Authorization Server:
|
||||
|
||||
[options="header"]
|
||||
|======================================
|
||||
| Resource Path | Description
|
||||
| /nifi-api/access/oidc/callback/consumer | Process OIDC 1.0 Login Authentication Responses from an Authentication Server.
|
||||
| /nifi/logout-complete | Path for redirect after successful OIDC RP-Initiated Logout 1.0 processing
|
||||
|======================================
|
||||
|
||||
[[saml]]
|
||||
=== SAML
|
||||
|
||||
|
|
|
@ -173,6 +173,7 @@
|
|||
<nifi.security.user.oidc.fallback.claims.identifying.user />
|
||||
<nifi.security.user.oidc.claim.groups>groups</nifi.security.user.oidc.claim.groups>
|
||||
<nifi.security.user.oidc.truststore.strategy>JDK</nifi.security.user.oidc.truststore.strategy>
|
||||
<nifi.security.user.oidc.token.refresh.window>60 secs</nifi.security.user.oidc.token.refresh.window>
|
||||
|
||||
<!-- nifi.properties: apache knox -->
|
||||
<nifi.security.user.knox.url />
|
||||
|
|
|
@ -204,6 +204,7 @@ nifi.security.user.oidc.claim.identifying.user=${nifi.security.user.oidc.claim.i
|
|||
nifi.security.user.oidc.fallback.claims.identifying.user=${nifi.security.user.oidc.fallback.claims.identifying.user}
|
||||
nifi.security.user.oidc.claim.groups=${nifi.security.user.oidc.claim.groups}
|
||||
nifi.security.user.oidc.truststore.strategy=${nifi.security.user.oidc.truststore.strategy}
|
||||
nifi.security.user.oidc.token.refresh.window=${nifi.security.user.oidc.token.refresh.window}
|
||||
|
||||
# Apache Knox SSO Properties #
|
||||
nifi.security.user.knox.url=${nifi.security.user.knox.url}
|
||||
|
|
|
@ -16,18 +16,17 @@
|
|||
*/
|
||||
package org.apache.nifi.web;
|
||||
|
||||
import org.apache.nifi.web.security.configuration.AuthenticationSecurityConfiguration;
|
||||
import org.apache.nifi.web.security.configuration.WebSecurityConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.ImportResource;
|
||||
|
||||
/**
|
||||
*
|
||||
* Web Application Spring Configuration
|
||||
*/
|
||||
@Configuration
|
||||
@Import({
|
||||
NiFiWebApiSecurityConfiguration.class,
|
||||
AuthenticationSecurityConfiguration.class
|
||||
WebSecurityConfiguration.class
|
||||
})
|
||||
@ImportResource({"classpath:nifi-context.xml",
|
||||
"classpath:nifi-administration-context.xml",
|
||||
|
|
|
@ -98,7 +98,6 @@ public class NiFiWebApiResourceConfig extends ResourceConfig {
|
|||
register(ctx.getBean("countersResource"));
|
||||
register(ctx.getBean("systemDiagnosticsResource"));
|
||||
register(ctx.getBean("accessResource"));
|
||||
register(ctx.getBean("oidcResource"));
|
||||
register(ctx.getBean("accessPolicyResource"));
|
||||
register(ctx.getBean("tenantsResource"));
|
||||
register(ctx.getBean("versionsResource"));
|
||||
|
|
|
@ -1,578 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.api;
|
||||
|
||||
import com.nimbusds.oauth2.sdk.AuthorizationCode;
|
||||
import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
|
||||
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
|
||||
import com.nimbusds.oauth2.sdk.ParseException;
|
||||
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
|
||||
import com.nimbusds.oauth2.sdk.id.State;
|
||||
import com.nimbusds.openid.connect.sdk.AuthenticationResponse;
|
||||
import com.nimbusds.openid.connect.sdk.AuthenticationResponseParser;
|
||||
import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.apache.nifi.admin.service.IdpUserGroupService;
|
||||
import org.apache.nifi.authentication.exception.AuthenticationNotSupportedException;
|
||||
import org.apache.nifi.authorization.user.NiFiUserUtils;
|
||||
import org.apache.nifi.idp.IdpType;
|
||||
import org.apache.nifi.security.util.SslContextFactory;
|
||||
import org.apache.nifi.security.util.StandardTlsConfiguration;
|
||||
import org.apache.nifi.security.util.TlsConfiguration;
|
||||
import org.apache.nifi.security.util.TlsException;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider;
|
||||
import org.apache.nifi.web.security.oidc.OIDCEndpoints;
|
||||
import org.apache.nifi.web.security.oidc.OidcService;
|
||||
import org.apache.nifi.web.security.oidc.TruststoreStrategy;
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
@Path(OIDCEndpoints.OIDC_ACCESS_ROOT)
|
||||
@Api(
|
||||
value = OIDCEndpoints.OIDC_ACCESS_ROOT,
|
||||
description = "Endpoints for obtaining an access token or checking access status."
|
||||
)
|
||||
public class OIDCAccessResource extends ApplicationResource {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OIDCAccessResource.class);
|
||||
private static final String OIDC_AUTHENTICATION_NOT_CONFIGURED = "OIDC authentication not configured";
|
||||
private static final String OIDC_ID_TOKEN_AUTHN_ERROR = "Unable to exchange authorization for ID token: ";
|
||||
private static final String OIDC_REQUEST_IDENTIFIER_NOT_FOUND = "The request identifier was not found in the request.";
|
||||
private static final String OIDC_FAILED_TO_PARSE_REDIRECT_URI = "Unable to parse the redirect URI from the OpenId Connect Provider. Unable to continue login/logout process.";
|
||||
private static final String REVOKE_ACCESS_TOKEN_LOGOUT = "oidc_access_token_logout";
|
||||
private static final String ID_TOKEN_LOGOUT = "oidc_id_token_logout";
|
||||
private static final String STANDARD_LOGOUT = "oidc_standard_logout";
|
||||
private static final int msTimeout = 30_000;
|
||||
private static final boolean LOGGING_IN = true;
|
||||
|
||||
private OidcService oidcService;
|
||||
private IdpUserGroupService idpUserGroupService;
|
||||
private BearerTokenProvider bearerTokenProvider;
|
||||
|
||||
public OIDCAccessResource() {
|
||||
|
||||
}
|
||||
|
||||
@GET
|
||||
@Consumes(MediaType.WILDCARD)
|
||||
@Produces(MediaType.WILDCARD)
|
||||
@Path(OIDCEndpoints.LOGIN_REQUEST_RELATIVE)
|
||||
@ApiOperation(
|
||||
value = "Initiates a request to authenticate through the configured OpenId Connect provider.",
|
||||
notes = NON_GUARANTEED_ENDPOINT
|
||||
)
|
||||
public void oidcRequest(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
|
||||
|
||||
try {
|
||||
validateOidcConfiguration();
|
||||
} catch (AuthenticationNotSupportedException e) {
|
||||
forwardToLoginMessagePage(httpServletRequest, httpServletResponse, e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
|
||||
// generate the authorization uri
|
||||
URI authorizationURI = oidcRequestAuthorizationCode(httpServletResponse, getOidcCallback());
|
||||
|
||||
// generate the response
|
||||
httpServletResponse.sendRedirect(authorizationURI.toString());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Consumes(MediaType.WILDCARD)
|
||||
@Produces(MediaType.WILDCARD)
|
||||
@Path(OIDCEndpoints.LOGIN_CALLBACK_RELATIVE)
|
||||
@ApiOperation(
|
||||
value = "Redirect/callback URI for processing the result of the OpenId Connect login sequence.",
|
||||
notes = NON_GUARANTEED_ENDPOINT
|
||||
)
|
||||
public void oidcCallback(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
|
||||
|
||||
final AuthenticationResponse oidcResponse = parseOidcResponse(httpServletRequest, httpServletResponse, LOGGING_IN);
|
||||
final Optional<String> requestIdentifier = getOidcRequestIdentifier(httpServletRequest);
|
||||
|
||||
if (requestIdentifier.isPresent() && oidcResponse.indicatesSuccess()) {
|
||||
final AuthenticationSuccessResponse successfulOidcResponse = (AuthenticationSuccessResponse) oidcResponse;
|
||||
|
||||
final String oidcRequestIdentifier = requestIdentifier.get();
|
||||
checkOidcState(httpServletResponse, oidcRequestIdentifier, successfulOidcResponse, LOGGING_IN);
|
||||
|
||||
try {
|
||||
// exchange authorization code for id token
|
||||
final AuthorizationCode authorizationCode = successfulOidcResponse.getAuthorizationCode();
|
||||
final AuthorizationGrant authorizationGrant = new AuthorizationCodeGrant(authorizationCode, URI.create(getOidcCallback()));
|
||||
|
||||
// get the oidc token
|
||||
LoginAuthenticationToken oidcToken = oidcService.exchangeAuthorizationCodeForLoginAuthenticationToken(authorizationGrant);
|
||||
|
||||
// exchange the oidc token for the NiFi token
|
||||
final String bearerToken = bearerTokenProvider.getBearerToken(oidcToken);
|
||||
|
||||
// store the NiFi token
|
||||
oidcService.storeJwt(oidcRequestIdentifier, bearerToken);
|
||||
|
||||
Set<String> groups = oidcToken.getAuthorities().stream().map(authority -> authority.getAuthority()).collect(Collectors.collectingAndThen(
|
||||
Collectors.toSet(),
|
||||
Collections::unmodifiableSet
|
||||
));
|
||||
idpUserGroupService.replaceUserGroups(oidcToken.getName(), IdpType.OIDC, groups);
|
||||
} catch (final Exception e) {
|
||||
logger.error(OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage(), e);
|
||||
|
||||
// remove the oidc request cookie
|
||||
removeOidcRequestCookie(httpServletResponse);
|
||||
|
||||
// forward to the error page
|
||||
forwardToLoginMessagePage(httpServletRequest, httpServletResponse, OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
// redirect to the name page
|
||||
httpServletResponse.sendRedirect(getNiFiUri());
|
||||
} else {
|
||||
// remove the oidc request cookie
|
||||
removeOidcRequestCookie(httpServletResponse);
|
||||
|
||||
// report the unsuccessful login
|
||||
forwardToLoginMessagePage(httpServletRequest, httpServletResponse, "Unsuccessful login attempt.");
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.WILDCARD)
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
@Path(OIDCEndpoints.TOKEN_EXCHANGE_RELATIVE)
|
||||
@ApiOperation(
|
||||
value = "Retrieves a JWT following a successful login sequence using the configured OpenId Connect provider.",
|
||||
response = String.class,
|
||||
notes = NON_GUARANTEED_ENDPOINT
|
||||
)
|
||||
public Response oidcExchange(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) {
|
||||
|
||||
try {
|
||||
validateOidcConfiguration();
|
||||
} catch (final AuthenticationNotSupportedException e) {
|
||||
logger.debug("OIDC authentication not supported", e);
|
||||
return Response.status(Response.Status.CONFLICT).entity(e.getMessage()).build();
|
||||
}
|
||||
|
||||
final Optional<String> requestIdentifier = getOidcRequestIdentifier(httpServletRequest);
|
||||
if (!requestIdentifier.isPresent()) {
|
||||
final String message = "The login request identifier was not found in the request. Unable to continue.";
|
||||
logger.warn(message);
|
||||
return Response.status(Response.Status.BAD_REQUEST).entity(message).build();
|
||||
}
|
||||
|
||||
// remove the oidc request cookie
|
||||
removeOidcRequestCookie(httpServletResponse);
|
||||
|
||||
// get the jwt
|
||||
final String jwt = oidcService.getJwt(requestIdentifier.get());
|
||||
if (jwt == null) {
|
||||
throw new IllegalArgumentException("A JWT for this login request identifier could not be found. Unable to continue.");
|
||||
}
|
||||
|
||||
setBearerToken(httpServletResponse, jwt);
|
||||
return generateOkResponse(jwt).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Consumes(MediaType.WILDCARD)
|
||||
@Produces(MediaType.WILDCARD)
|
||||
@Path(OIDCEndpoints.LOGOUT_REQUEST_RELATIVE)
|
||||
@ApiOperation(
|
||||
value = "Performs a logout in the OpenId Provider.",
|
||||
notes = NON_GUARANTEED_ENDPOINT
|
||||
)
|
||||
public void oidcLogout(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
|
||||
|
||||
try {
|
||||
validateOidcConfiguration();
|
||||
} catch (final AuthenticationNotSupportedException e) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
final String mappedUserIdentity = NiFiUserUtils.getNiFiUserIdentity();
|
||||
applicationCookieService.removeCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.AUTHORIZATION_BEARER);
|
||||
logger.debug("Invalidated JWT for user [{}]", mappedUserIdentity);
|
||||
|
||||
idpUserGroupService.deleteUserGroups(mappedUserIdentity);
|
||||
logger.debug("Deleted user groups for user [{}]", mappedUserIdentity);
|
||||
|
||||
// Determine the logout method
|
||||
String logoutMethod = determineLogoutMethod();
|
||||
|
||||
switch (logoutMethod) {
|
||||
case REVOKE_ACCESS_TOKEN_LOGOUT:
|
||||
case ID_TOKEN_LOGOUT:
|
||||
// Make a request to the IdP
|
||||
URI authorizationURI = oidcRequestAuthorizationCode(httpServletResponse, getOidcLogoutCallback());
|
||||
httpServletResponse.sendRedirect(authorizationURI.toString());
|
||||
break;
|
||||
case STANDARD_LOGOUT:
|
||||
default:
|
||||
// Get the OIDC end session endpoint
|
||||
URI endSessionEndpoint = oidcService.getEndSessionEndpoint();
|
||||
String postLogoutRedirectUri = generateResourceUri( "..", "nifi", "logout-complete");
|
||||
|
||||
if (endSessionEndpoint == null) {
|
||||
httpServletResponse.sendRedirect(postLogoutRedirectUri);
|
||||
} else {
|
||||
URI logoutUri = UriBuilder.fromUri(endSessionEndpoint)
|
||||
.queryParam("post_logout_redirect_uri", postLogoutRedirectUri)
|
||||
.build();
|
||||
httpServletResponse.sendRedirect(logoutUri.toString());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Consumes(MediaType.WILDCARD)
|
||||
@Produces(MediaType.WILDCARD)
|
||||
@Path(OIDCEndpoints.LOGOUT_CALLBACK_RELATIVE)
|
||||
@ApiOperation(
|
||||
value = "Redirect/callback URI for processing the result of the OpenId Connect logout sequence.",
|
||||
notes = NON_GUARANTEED_ENDPOINT
|
||||
)
|
||||
public void oidcLogoutCallback(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
|
||||
final AuthenticationResponse oidcResponse = parseOidcResponse(httpServletRequest, httpServletResponse, !LOGGING_IN);
|
||||
final Optional<String> requestIdentifier = getOidcRequestIdentifier(httpServletRequest);
|
||||
|
||||
if (requestIdentifier.isPresent() && oidcResponse != null && oidcResponse.indicatesSuccess()) {
|
||||
final AuthenticationSuccessResponse successfulOidcResponse = (AuthenticationSuccessResponse) oidcResponse;
|
||||
|
||||
// confirm state
|
||||
final String oidcRequestIdentifier = requestIdentifier.get();
|
||||
checkOidcState(httpServletResponse, oidcRequestIdentifier, successfulOidcResponse, false);
|
||||
|
||||
// Determine which logout method to use
|
||||
String logoutMethod = determineLogoutMethod();
|
||||
|
||||
// Get the authorization code and grant
|
||||
final AuthorizationCode authorizationCode = successfulOidcResponse.getAuthorizationCode();
|
||||
final AuthorizationGrant authorizationGrant = new AuthorizationCodeGrant(authorizationCode, URI.create(getOidcLogoutCallback()));
|
||||
|
||||
switch (logoutMethod) {
|
||||
case REVOKE_ACCESS_TOKEN_LOGOUT:
|
||||
// Use the Revocation endpoint + access token
|
||||
final String accessToken;
|
||||
try {
|
||||
// Return the access token
|
||||
accessToken = oidcService.exchangeAuthorizationCodeForAccessToken(authorizationGrant);
|
||||
} catch (final Exception e) {
|
||||
logger.error("Unable to exchange authorization for the Access token: " + e.getMessage(), e);
|
||||
|
||||
// Remove the oidc request cookie
|
||||
removeOidcRequestCookie(httpServletResponse);
|
||||
|
||||
// Forward to the error page
|
||||
forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
// Build the revoke URI and send the POST request
|
||||
URI revokeEndpoint = getRevokeEndpoint();
|
||||
|
||||
if (revokeEndpoint != null) {
|
||||
try {
|
||||
// Logout with the revoke endpoint
|
||||
revokeEndpointRequest(httpServletResponse, accessToken, revokeEndpoint);
|
||||
|
||||
} catch (final IOException e) {
|
||||
logger.error("There was an error logging out of the OpenId Connect Provider: "
|
||||
+ e.getMessage(), e);
|
||||
|
||||
// Remove the oidc request cookie
|
||||
removeOidcRequestCookie(httpServletResponse);
|
||||
|
||||
// Forward to the error page
|
||||
forwardToLogoutMessagePage(httpServletRequest, httpServletResponse,
|
||||
"There was an error logging out of the OpenId Connect Provider: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ID_TOKEN_LOGOUT:
|
||||
// Use the end session endpoint + ID Token
|
||||
final String idToken;
|
||||
try {
|
||||
// Return the ID Token
|
||||
idToken = oidcService.exchangeAuthorizationCodeForIdToken(authorizationGrant);
|
||||
} catch (final Exception e) {
|
||||
logger.error(OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage(), e);
|
||||
|
||||
// Remove the oidc request cookie
|
||||
removeOidcRequestCookie(httpServletResponse);
|
||||
|
||||
// Forward to the error page
|
||||
forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the OIDC end session endpoint
|
||||
URI endSessionEndpoint = oidcService.getEndSessionEndpoint();
|
||||
String postLogoutRedirectUri = generateResourceUri("..", "nifi", "logout-complete");
|
||||
|
||||
if (endSessionEndpoint == null) {
|
||||
logger.debug("Unable to log out of the OpenId Connect Provider. The end session endpoint is: null." +
|
||||
" Redirecting to the logout page.");
|
||||
httpServletResponse.sendRedirect(postLogoutRedirectUri);
|
||||
} else {
|
||||
URI logoutUri = UriBuilder.fromUri(endSessionEndpoint)
|
||||
.queryParam("id_token_hint", idToken)
|
||||
.queryParam("post_logout_redirect_uri", postLogoutRedirectUri)
|
||||
.build();
|
||||
httpServletResponse.sendRedirect(logoutUri.toString());
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// remove the oidc request cookie
|
||||
removeOidcRequestCookie(httpServletResponse);
|
||||
|
||||
// report the unsuccessful logout
|
||||
forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, "Unsuccessful logout attempt.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the request Authorization URI for the OpenID Connect Provider. Returns an authorization
|
||||
* URI using the provided callback URI.
|
||||
*
|
||||
* @param httpServletResponse the servlet response
|
||||
* @param callback the OIDC callback URI
|
||||
* @return the authorization URI
|
||||
*/
|
||||
private URI oidcRequestAuthorizationCode(@Context HttpServletResponse httpServletResponse, String callback) {
|
||||
final String oidcRequestIdentifier = UUID.randomUUID().toString();
|
||||
applicationCookieService.addCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.OIDC_REQUEST_IDENTIFIER, oidcRequestIdentifier);
|
||||
final State state = oidcService.createState(oidcRequestIdentifier);
|
||||
return UriBuilder.fromUri(oidcService.getAuthorizationEndpoint())
|
||||
.queryParam("client_id", oidcService.getClientId())
|
||||
.queryParam("response_type", "code")
|
||||
.queryParam("scope", oidcService.getScope().toString())
|
||||
.queryParam("state", state.getValue())
|
||||
.queryParam("redirect_uri", callback)
|
||||
.build();
|
||||
}
|
||||
|
||||
private String determineLogoutMethod() {
|
||||
if (oidcService.getEndSessionEndpoint() != null) {
|
||||
return ID_TOKEN_LOGOUT;
|
||||
} else if (oidcService.getRevocationEndpoint() != null) {
|
||||
return REVOKE_ACCESS_TOKEN_LOGOUT;
|
||||
} else {
|
||||
return STANDARD_LOGOUT;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a POST request to the revoke endpoint to log out of the ID Provider.
|
||||
*
|
||||
* @param httpServletResponse the servlet response
|
||||
* @param accessToken the OpenID Connect Provider access token
|
||||
* @param revokeEndpoint the name of the cookie
|
||||
* @throws IOException exceptional case for communication error with the OpenId Connect Provider
|
||||
*/
|
||||
private void revokeEndpointRequest(@Context HttpServletResponse httpServletResponse, String accessToken, URI revokeEndpoint) throws IOException {
|
||||
final CloseableHttpClient httpClient = getHttpClient();
|
||||
HttpPost httpPost = new HttpPost(revokeEndpoint);
|
||||
|
||||
List<NameValuePair> params = new ArrayList<>();
|
||||
// Append a query param with the access token
|
||||
params.add(new BasicNameValuePair("token", accessToken));
|
||||
httpPost.setEntity(new UrlEncodedFormEntity(params));
|
||||
|
||||
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
|
||||
if (response.getStatusLine().getStatusCode() == HTTPResponse.SC_OK) {
|
||||
// Redirect to logout page
|
||||
logger.debug("You are logged out of the OpenId Connect Provider.");
|
||||
String postLogoutRedirectUri = generateResourceUri("..", "nifi", "logout-complete");
|
||||
httpServletResponse.sendRedirect(postLogoutRedirectUri);
|
||||
} else {
|
||||
logger.error("There was an error logging out of the OpenId Connect Provider. " +
|
||||
"Response status: " + response.getStatusLine().getStatusCode());
|
||||
}
|
||||
} finally {
|
||||
httpClient.close();
|
||||
}
|
||||
}
|
||||
|
||||
private CloseableHttpClient getHttpClient() {
|
||||
RequestConfig config = RequestConfig.custom()
|
||||
.setConnectTimeout(msTimeout)
|
||||
.setConnectionRequestTimeout(msTimeout)
|
||||
.setSocketTimeout(msTimeout)
|
||||
.build();
|
||||
|
||||
HttpClientBuilder builder = HttpClientBuilder
|
||||
.create()
|
||||
.setDefaultRequestConfig(config);
|
||||
|
||||
if (TruststoreStrategy.NIFI.name().equals(properties.getOidcClientTruststoreStrategy())) {
|
||||
builder.setSSLContext(getSslContext());
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
protected AuthenticationResponse parseOidcResponse(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, boolean isLogin) throws Exception {
|
||||
final String pageTitle = getForwardPageTitle(isLogin);
|
||||
|
||||
try {
|
||||
validateOidcConfiguration();
|
||||
} catch (final AuthenticationNotSupportedException e) {
|
||||
forwardToMessagePage(httpServletRequest, httpServletResponse, pageTitle, e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
|
||||
final Optional<String> requestIdentifier = getOidcRequestIdentifier(httpServletRequest);
|
||||
if (!requestIdentifier.isPresent()) {
|
||||
forwardToMessagePage(httpServletRequest, httpServletResponse, pageTitle, OIDC_REQUEST_IDENTIFIER_NOT_FOUND);
|
||||
throw new IllegalStateException(OIDC_REQUEST_IDENTIFIER_NOT_FOUND);
|
||||
}
|
||||
|
||||
final com.nimbusds.openid.connect.sdk.AuthenticationResponse oidcResponse;
|
||||
|
||||
try {
|
||||
oidcResponse = AuthenticationResponseParser.parse(getRequestUri());
|
||||
return oidcResponse;
|
||||
} catch (final ParseException e) {
|
||||
logger.error(OIDC_FAILED_TO_PARSE_REDIRECT_URI);
|
||||
|
||||
// remove the oidc request cookie
|
||||
removeOidcRequestCookie(httpServletResponse);
|
||||
|
||||
// forward to the error page
|
||||
forwardToMessagePage(httpServletRequest, httpServletResponse, pageTitle, OIDC_FAILED_TO_PARSE_REDIRECT_URI);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
protected void checkOidcState(HttpServletResponse httpServletResponse, final String oidcRequestIdentifier, AuthenticationSuccessResponse successfulOidcResponse, boolean isLogin) throws Exception {
|
||||
// confirm state
|
||||
final State state = successfulOidcResponse.getState();
|
||||
if (state == null || !oidcService.isStateValid(oidcRequestIdentifier, state)) {
|
||||
logger.error("OIDC Request [{}] State [{}] not valid", oidcRequestIdentifier, state);
|
||||
|
||||
removeOidcRequestCookie(httpServletResponse);
|
||||
|
||||
forwardToMessagePage(httpServletRequest, httpServletResponse, getForwardPageTitle(isLogin), "Purposed state does not match " +
|
||||
"the stored state. Unable to continue login/logout process.");
|
||||
}
|
||||
}
|
||||
|
||||
private SSLContext getSslContext() {
|
||||
TlsConfiguration tlsConfiguration = StandardTlsConfiguration.fromNiFiProperties(properties);
|
||||
try {
|
||||
return SslContextFactory.createSslContext(tlsConfiguration);
|
||||
} catch (TlsException e) {
|
||||
throw new RuntimeException("Unable to establish an SSL context for OIDC access resource from nifi.properties", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateOidcConfiguration() throws AuthenticationNotSupportedException {
|
||||
// only consider user specific access over https
|
||||
if (!httpServletRequest.isSecure()) {
|
||||
throw new AuthenticationNotSupportedException(AccessResource.AUTHENTICATION_NOT_ENABLED_MSG);
|
||||
}
|
||||
|
||||
// ensure OIDC is actually configured/enabled
|
||||
if (!oidcService.isOidcEnabled()) {
|
||||
throw new AuthenticationNotSupportedException(OIDC_AUTHENTICATION_NOT_CONFIGURED);
|
||||
}
|
||||
}
|
||||
|
||||
private String getForwardPageTitle(boolean isLogin) {
|
||||
return isLogin ? ApplicationResource.LOGIN_ERROR_TITLE : ApplicationResource.LOGOUT_ERROR_TITLE;
|
||||
}
|
||||
|
||||
protected String getOidcCallback() {
|
||||
return generateResourceUri("access", "oidc", "callback");
|
||||
}
|
||||
|
||||
private String getOidcLogoutCallback() {
|
||||
return generateResourceUri("access", "oidc", "logoutCallback");
|
||||
}
|
||||
|
||||
private URI getRevokeEndpoint() {
|
||||
return oidcService.getRevocationEndpoint();
|
||||
}
|
||||
|
||||
private void removeOidcRequestCookie(final HttpServletResponse httpServletResponse) {
|
||||
applicationCookieService.removeCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.OIDC_REQUEST_IDENTIFIER);
|
||||
}
|
||||
|
||||
private Optional<String> getOidcRequestIdentifier(final HttpServletRequest request) {
|
||||
return applicationCookieService.getCookieValue(request, ApplicationCookieName.OIDC_REQUEST_IDENTIFIER);
|
||||
}
|
||||
|
||||
public void setOidcService(OidcService oidcService) {
|
||||
this.oidcService = oidcService;
|
||||
}
|
||||
|
||||
public void setIdpUserGroupService(IdpUserGroupService idpUserGroupService) {
|
||||
this.idpUserGroupService = idpUserGroupService;
|
||||
}
|
||||
|
||||
public void setBearerTokenProvider(final BearerTokenProvider bearerTokenProvider) {
|
||||
this.bearerTokenProvider = bearerTokenProvider;
|
||||
}
|
||||
|
||||
public void setProperties(final NiFiProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
protected NiFiProperties getProperties() {
|
||||
return properties;
|
||||
}
|
||||
}
|
|
@ -627,12 +627,6 @@
|
|||
<property name="requestReplicator" ref="requestReplicator" />
|
||||
<property name="flowController" ref="flowController" />
|
||||
</bean>
|
||||
<bean id="oidcResource" class="org.apache.nifi.web.api.OIDCAccessResource" scope="singleton">
|
||||
<property name="oidcService" ref="oidcService"/>
|
||||
<property name="idpUserGroupService" ref="idpUserGroupService" />
|
||||
<property name="properties" ref="nifiProperties"/>
|
||||
<property name="bearerTokenProvider" ref="bearerTokenProvider"/>
|
||||
</bean>
|
||||
<bean id="accessPolicyResource" class="org.apache.nifi.web.api.AccessPolicyResource" scope="singleton">
|
||||
<constructor-arg ref="serviceFacade"/>
|
||||
<constructor-arg ref="authorizer"/>
|
||||
|
|
|
@ -1,159 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.api;
|
||||
|
||||
import com.nimbusds.oauth2.sdk.AuthorizationCode;
|
||||
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
|
||||
import com.nimbusds.oauth2.sdk.ErrorObject;
|
||||
import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse;
|
||||
import com.nimbusds.openid.connect.sdk.AuthenticationResponse;
|
||||
import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
|
||||
import org.apache.nifi.admin.service.IdpUserGroupService;
|
||||
import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider;
|
||||
import org.apache.nifi.web.security.jwt.provider.StandardBearerTokenProvider;
|
||||
import org.apache.nifi.web.security.oidc.OidcService;
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.apache.nifi.idp.IdpType.OIDC;
|
||||
import static org.apache.nifi.web.security.cookie.ApplicationCookieName.OIDC_REQUEST_IDENTIFIER;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
|
||||
public class OIDCAccessResourceTest {
|
||||
|
||||
final static String REQUEST_IDENTIFIER = "an-identifier";
|
||||
|
||||
final static String OIDC_LOGIN_FAILURE_MESSAGE = "Unsuccessful login attempt.";
|
||||
|
||||
@Test
|
||||
public void testOidcCallbackSuccess() throws Exception {
|
||||
HttpServletRequest mockRequest = Mockito.mock(HttpServletRequest.class);
|
||||
MockHttpServletResponse mockResponse = new MockHttpServletResponse();
|
||||
Cookie[] cookies = { new Cookie(OIDC_REQUEST_IDENTIFIER.getCookieName(), REQUEST_IDENTIFIER) };
|
||||
Mockito.when(mockRequest.getCookies()).thenReturn(cookies);
|
||||
OidcService oidcService = Mockito.mock(OidcService.class);
|
||||
MockOIDCAccessResource accessResource = new MockOIDCAccessResource(oidcService, true);
|
||||
IdpUserGroupService idpUserGroupService = Mockito.mock(IdpUserGroupService.class);
|
||||
accessResource.setIdpUserGroupService(idpUserGroupService);
|
||||
accessResource.oidcCallback(mockRequest, mockResponse);
|
||||
Mockito.verify(oidcService).storeJwt(any(String.class), any(String.class));
|
||||
Mockito.verify(idpUserGroupService).replaceUserGroups(MockOIDCAccessResource.IDENTITY, OIDC, Stream.of(MockOIDCAccessResource.ROLE).collect(Collectors.toSet()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOidcCallbackFailure() throws Exception {
|
||||
HttpServletRequest mockRequest = Mockito.mock(HttpServletRequest.class);
|
||||
MockHttpServletResponse mockResponse = new MockHttpServletResponse();
|
||||
Cookie[] cookies = { new Cookie(OIDC_REQUEST_IDENTIFIER.getCookieName(), REQUEST_IDENTIFIER) };
|
||||
Mockito.when(mockRequest.getCookies()).thenReturn(cookies);
|
||||
OidcService oidcService = Mockito.mock(OidcService.class);
|
||||
MockOIDCAccessResource accessResource = new MockOIDCCallbackFailure(oidcService, false);
|
||||
accessResource.oidcCallback(mockRequest, mockResponse);
|
||||
}
|
||||
|
||||
public class MockOIDCCallbackFailure extends MockOIDCAccessResource {
|
||||
|
||||
public MockOIDCCallbackFailure(OidcService oidcService, Boolean requestShouldSucceed) throws IOException {
|
||||
super(oidcService, requestShouldSucceed);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void forwardToLoginMessagePage(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse, final String message) throws Exception {
|
||||
assertEquals(OIDC_LOGIN_FAILURE_MESSAGE, message);
|
||||
}
|
||||
}
|
||||
|
||||
public class MockOIDCAccessResource extends OIDCAccessResource {
|
||||
|
||||
final static String BEARER_TOKEN = "bearer_token";
|
||||
final static String IDENTITY = "identity";
|
||||
final static String ROLE = "role";
|
||||
final static String AUTHORIZATION_CODE = "authorization_code";
|
||||
final static String CALLBACK_URL = "https://nifi.apache.org/nifi-api/access/oidc/callback";
|
||||
final static String RESOURCE_URI = "resource_uri";
|
||||
private Boolean requestShouldSucceed;
|
||||
|
||||
public MockOIDCAccessResource(final OidcService oidcService, final Boolean requestShouldSucceed) throws IOException {
|
||||
this.requestShouldSucceed = requestShouldSucceed;
|
||||
final BearerTokenProvider bearerTokenProvider = Mockito.mock(StandardBearerTokenProvider.class);
|
||||
Mockito.when(bearerTokenProvider.getBearerToken(any(LoginAuthenticationToken.class))).thenReturn(BEARER_TOKEN);
|
||||
setOidcService(oidcService);
|
||||
setBearerTokenProvider(bearerTokenProvider);
|
||||
final LoginAuthenticationToken token = Mockito.mock(LoginAuthenticationToken.class);
|
||||
Mockito.when(token.getName()).thenReturn(IDENTITY);
|
||||
Mockito.when(token.getAuthorities()).thenReturn(Stream.of(new SimpleGrantedAuthority(ROLE)).collect(Collectors.toSet()));
|
||||
Mockito.when(oidcService.exchangeAuthorizationCodeForLoginAuthenticationToken(any(AuthorizationGrant.class))).thenReturn(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AuthenticationResponse parseOidcResponse(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, boolean isLogin) {
|
||||
if (requestShouldSucceed) {
|
||||
return getSuccessResponse();
|
||||
} else {
|
||||
return getErrorResponse();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void checkOidcState(HttpServletResponse httpServletResponse, final String oidcRequestIdentifier, AuthenticationSuccessResponse successfulOidcResponse, boolean isLogin)
|
||||
throws Exception {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getOidcCallback() {
|
||||
return CALLBACK_URL;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String generateResourceUri(final String... path) {
|
||||
return RESOURCE_URI;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected URI getCookieResourceUri() {
|
||||
return URI.create(RESOURCE_URI);
|
||||
}
|
||||
|
||||
private AuthenticationResponse getSuccessResponse() {
|
||||
AuthenticationSuccessResponse successResponse = Mockito.mock(AuthenticationSuccessResponse.class);
|
||||
Mockito.when(successResponse.indicatesSuccess()).thenReturn(true);
|
||||
Mockito.when(successResponse.getAuthorizationCode()).thenReturn(new AuthorizationCode(AUTHORIZATION_CODE));
|
||||
return successResponse;
|
||||
}
|
||||
|
||||
private AuthenticationResponse getErrorResponse() {
|
||||
AuthenticationErrorResponse errorResponse = Mockito.mock(AuthenticationErrorResponse.class);
|
||||
Mockito.when(errorResponse.indicatesSuccess()).thenReturn(false);
|
||||
Mockito.when(errorResponse.getErrorObject()).thenReturn(new ErrorObject("HTTP 500", "OIDC server error"));
|
||||
return errorResponse;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -194,6 +194,10 @@
|
|||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-jose</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>jcl-over-slf4j</artifactId>
|
||||
|
|
|
@ -37,7 +37,7 @@ import org.springframework.security.authentication.AuthenticationManager;
|
|||
JwtAuthenticationSecurityConfiguration.class,
|
||||
KerberosAuthenticationSecurityConfiguration.class,
|
||||
KnoxAuthenticationSecurityConfiguration.class,
|
||||
OidcAuthenticationSecurityConfiguration.class,
|
||||
OidcSecurityConfiguration.class,
|
||||
SamlAuthenticationSecurityConfiguration.class,
|
||||
X509AuthenticationSecurityConfiguration.class
|
||||
})
|
||||
|
|
|
@ -59,8 +59,8 @@ import org.springframework.security.oauth2.jwt.JwtValidators;
|
|||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
|
||||
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
|
@ -228,7 +228,7 @@ public class JwtAuthenticationSecurityConfiguration {
|
|||
@Bean
|
||||
public ThreadPoolTaskScheduler commandScheduler() {
|
||||
final ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
scheduler.setThreadNamePrefix(getClass().getSimpleName());
|
||||
scheduler.setThreadNamePrefix(JwtAuthenticationSecurityConfiguration.class.getSimpleName());
|
||||
return scheduler;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.configuration;
|
||||
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.oidc.OidcService;
|
||||
import org.apache.nifi.web.security.oidc.StandardOidcIdentityProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* OIDC Configuration for Authentication Security
|
||||
*/
|
||||
@Configuration
|
||||
public class OidcAuthenticationSecurityConfiguration {
|
||||
private final NiFiProperties niFiProperties;
|
||||
|
||||
@Autowired
|
||||
public OidcAuthenticationSecurityConfiguration(
|
||||
final NiFiProperties niFiProperties
|
||||
) {
|
||||
this.niFiProperties = niFiProperties;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public StandardOidcIdentityProvider oidcProvider() {
|
||||
return new StandardOidcIdentityProvider(niFiProperties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OidcService oidcService() {
|
||||
return new OidcService(oidcProvider());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,516 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.configuration;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import okhttp3.OkHttpClient;
|
||||
import org.apache.nifi.admin.service.IdpUserGroupService;
|
||||
import org.apache.nifi.authorization.util.IdentityMappingUtil;
|
||||
import org.apache.nifi.components.state.StateManager;
|
||||
import org.apache.nifi.components.state.StateManagerProvider;
|
||||
import org.apache.nifi.encrypt.PropertyEncryptor;
|
||||
import org.apache.nifi.security.util.SslContextFactory;
|
||||
import org.apache.nifi.security.util.StandardTlsConfiguration;
|
||||
import org.apache.nifi.security.util.TlsConfiguration;
|
||||
import org.apache.nifi.security.util.TlsException;
|
||||
import org.apache.nifi.util.FormatUtils;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.util.StringUtils;
|
||||
import org.apache.nifi.web.security.StandardAuthenticationEntryPoint;
|
||||
import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider;
|
||||
import org.apache.nifi.web.security.logout.LogoutRequestManager;
|
||||
import org.apache.nifi.web.security.oidc.OidcConfigurationException;
|
||||
import org.apache.nifi.web.security.oidc.OidcUrlPath;
|
||||
import org.apache.nifi.web.security.oidc.client.web.AuthorizedClientExpirationCommand;
|
||||
import org.apache.nifi.web.security.oidc.client.web.OidcBearerTokenRefreshFilter;
|
||||
import org.apache.nifi.web.security.oidc.client.web.converter.AuthenticationResultConverter;
|
||||
import org.apache.nifi.web.security.oidc.client.web.converter.AuthorizedClientConverter;
|
||||
import org.apache.nifi.web.security.oidc.client.web.StandardAuthorizationRequestRepository;
|
||||
import org.apache.nifi.web.security.oidc.client.web.converter.StandardAuthorizedClientConverter;
|
||||
import org.apache.nifi.web.security.oidc.client.web.StandardOidcAuthorizedClientRepository;
|
||||
import org.apache.nifi.web.security.oidc.logout.OidcLogoutFilter;
|
||||
import org.apache.nifi.web.security.oidc.logout.OidcLogoutSuccessHandler;
|
||||
import org.apache.nifi.web.security.oidc.registration.ClientRegistrationProvider;
|
||||
import org.apache.nifi.web.security.oidc.registration.DisabledClientRegistrationRepository;
|
||||
import org.apache.nifi.web.security.oidc.registration.StandardClientRegistrationProvider;
|
||||
import org.apache.nifi.web.security.oidc.revocation.StandardTokenRevocationResponseClient;
|
||||
import org.apache.nifi.web.security.oidc.revocation.TokenRevocationResponseClient;
|
||||
import org.apache.nifi.web.security.oidc.web.authentication.OidcAuthenticationSuccessHandler;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cache.caffeine.CaffeineCache;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
|
||||
import org.springframework.http.converter.FormHttpMessageConverter;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
||||
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory;
|
||||
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
|
||||
import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
|
||||
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
|
||||
import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
|
||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||
import org.springframework.security.web.savedrequest.RequestCache;
|
||||
import org.springframework.web.client.RestOperations;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* OpenID Connect Configuration for Spring Security
|
||||
*/
|
||||
@Configuration
|
||||
public class OidcSecurityConfiguration {
|
||||
private static final Duration REQUEST_EXPIRATION = Duration.ofSeconds(60);
|
||||
|
||||
private static final long AUTHORIZATION_REQUEST_CACHE_SIZE = 1000;
|
||||
|
||||
private static final Duration DEFAULT_SOCKET_TIMEOUT = Duration.ofSeconds(5);
|
||||
|
||||
private static final String NIFI_TRUSTSTORE_STRATEGY = "NIFI";
|
||||
|
||||
private static final RequestCache nullRequestCache = new NullRequestCache();
|
||||
|
||||
private final Duration keyRotationPeriod;
|
||||
|
||||
private final NiFiProperties properties;
|
||||
|
||||
private final StateManagerProvider stateManagerProvider;
|
||||
|
||||
private final PropertyEncryptor propertyEncryptor;
|
||||
|
||||
private final BearerTokenProvider bearerTokenProvider;
|
||||
|
||||
private final BearerTokenResolver bearerTokenResolver;
|
||||
|
||||
private final IdpUserGroupService idpUserGroupService;
|
||||
|
||||
private final JwtDecoder jwtDecoder;
|
||||
|
||||
private final LogoutRequestManager logoutRequestManager;
|
||||
|
||||
@Autowired
|
||||
public OidcSecurityConfiguration(
|
||||
final NiFiProperties properties,
|
||||
final StateManagerProvider stateManagerProvider,
|
||||
final PropertyEncryptor propertyEncryptor,
|
||||
final BearerTokenProvider bearerTokenProvider,
|
||||
final BearerTokenResolver bearerTokenResolver,
|
||||
final IdpUserGroupService idpUserGroupService,
|
||||
final JwtDecoder jwtDecoder,
|
||||
final LogoutRequestManager logoutRequestManager
|
||||
) {
|
||||
this.properties = Objects.requireNonNull(properties, "Properties required");
|
||||
this.stateManagerProvider = Objects.requireNonNull(stateManagerProvider, "State Manager Provider required");
|
||||
this.propertyEncryptor = Objects.requireNonNull(propertyEncryptor, "Property Encryptor required");
|
||||
this.bearerTokenProvider = Objects.requireNonNull(bearerTokenProvider, "Bearer Token Provider required");
|
||||
this.bearerTokenResolver = Objects.requireNonNull(bearerTokenResolver, "Bearer Token Resolver required");
|
||||
this.idpUserGroupService = Objects.requireNonNull(idpUserGroupService, "User Group Service required");
|
||||
this.jwtDecoder = Objects.requireNonNull(jwtDecoder, "JWT Decoder required");
|
||||
this.logoutRequestManager = Objects.requireNonNull(logoutRequestManager, "Logout Request Manager required");
|
||||
this.keyRotationPeriod = properties.getSecurityUserJwsKeyRotationPeriod();
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization Code Grant Filter handles Authorization Server responses and updates the Authorized Client
|
||||
* Repository with ID Token and optional Refresh Token information
|
||||
*
|
||||
* @param authenticationManager Spring Security Authentication Manager
|
||||
* @return OAuth2 Authorization Code Grant Filter
|
||||
*/
|
||||
@Bean
|
||||
public OAuth2AuthorizationCodeGrantFilter oAuth2AuthorizationCodeGrantFilter(final AuthenticationManager authenticationManager) {
|
||||
final OAuth2AuthorizationCodeGrantFilter filter = new OAuth2AuthorizationCodeGrantFilter(
|
||||
clientRegistrationRepository(),
|
||||
authorizedClientRepository(),
|
||||
authenticationManager
|
||||
);
|
||||
filter.setAuthorizationRequestRepository(authorizationRequestRepository());
|
||||
filter.setRequestCache(nullRequestCache);
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization Request Redirect Filter handles initial OpenID Connect authentication and redirects to the
|
||||
* Authorization Server using default filter path from Spring Security
|
||||
*
|
||||
* @return OAuth2 Authorization Request Redirect Filter
|
||||
*/
|
||||
@Bean
|
||||
public OAuth2AuthorizationRequestRedirectFilter oAuth2AuthorizationRequestRedirectFilter() {
|
||||
final OAuth2AuthorizationRequestRedirectFilter filter = new OAuth2AuthorizationRequestRedirectFilter(clientRegistrationRepository());
|
||||
filter.setAuthorizationRequestRepository(authorizationRequestRepository());
|
||||
filter.setRequestCache(nullRequestCache);
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Login Authentication Filter handles Authentication Responses from the Authorization Server
|
||||
*
|
||||
* @param authenticationManager Spring Security Authentication Manager
|
||||
* @param authenticationEntryPoint Authentication Entry Point for handling failures
|
||||
* @return OAuth2 Login Authentication Filter
|
||||
*/
|
||||
@Bean
|
||||
public OAuth2LoginAuthenticationFilter oAuth2LoginAuthenticationFilter(final AuthenticationManager authenticationManager, final StandardAuthenticationEntryPoint authenticationEntryPoint) {
|
||||
final OAuth2LoginAuthenticationFilter filter = new OAuth2LoginAuthenticationFilter(
|
||||
clientRegistrationRepository(),
|
||||
authorizedClientRepository(),
|
||||
OidcUrlPath.CALLBACK.getPath()
|
||||
);
|
||||
filter.setAuthenticationManager(authenticationManager);
|
||||
filter.setAuthorizationRequestRepository(authorizationRequestRepository());
|
||||
filter.setAuthenticationSuccessHandler(getAuthenticationSuccessHandler());
|
||||
filter.setAllowSessionCreation(false);
|
||||
filter.setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy());
|
||||
filter.setAuthenticationResultConverter(new AuthenticationResultConverter());
|
||||
|
||||
final AuthenticationEntryPointFailureHandler authenticationFailureHandler = new AuthenticationEntryPointFailureHandler(authenticationEntryPoint);
|
||||
filter.setAuthenticationFailureHandler(authenticationFailureHandler);
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenID Connect Bearer Token Refresh Filter exchanges OAuth2 Refresh Tokens with the Authorization Server and
|
||||
* generates new application Bearer Tokens on successful responses
|
||||
*
|
||||
* @return Bearer Token Refresh Filter
|
||||
*/
|
||||
@Bean
|
||||
public OidcBearerTokenRefreshFilter oidcBearerTokenRefreshFilter() {
|
||||
final DefaultRefreshTokenTokenResponseClient refreshTokenResponseClient = new DefaultRefreshTokenTokenResponseClient();
|
||||
refreshTokenResponseClient.setRestOperations(oidcRestOperations());
|
||||
|
||||
final String refreshWindowProperty = properties.getOidcTokenRefreshWindow();
|
||||
final double refreshWindowSeconds = FormatUtils.getPreciseTimeDuration(refreshWindowProperty, TimeUnit.SECONDS);
|
||||
final Duration refreshWindow = Duration.ofSeconds(Math.round(refreshWindowSeconds));
|
||||
|
||||
return new OidcBearerTokenRefreshFilter(
|
||||
refreshWindow,
|
||||
bearerTokenProvider,
|
||||
bearerTokenResolver,
|
||||
jwtDecoder,
|
||||
authorizedClientRepository(),
|
||||
refreshTokenResponseClient
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout Filter for completing logout processing using RP-Initiated Logout 1.0 when supported
|
||||
*
|
||||
* @return OpenID Connect Logout Filter
|
||||
*/
|
||||
@Bean
|
||||
public OidcLogoutFilter oidcLogoutFilter() {
|
||||
return new OidcLogoutFilter(oidcLogoutSuccessHandler());
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout Success Handler redirects to the Authorization Server when supported
|
||||
*
|
||||
* @return Logout Success Handler
|
||||
*/
|
||||
@Bean
|
||||
public LogoutSuccessHandler oidcLogoutSuccessHandler() {
|
||||
return new OidcLogoutSuccessHandler(
|
||||
logoutRequestManager,
|
||||
idpUserGroupService,
|
||||
clientRegistrationRepository(),
|
||||
authorizedClientRepository(),
|
||||
tokenRevocationResponseClient()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization Code Grant Authentication Provider wired to Spring Security Authentication Manager
|
||||
*
|
||||
* @return OpenID Connect Authorization Code Authentication Provider
|
||||
*/
|
||||
@Bean
|
||||
public OidcAuthorizationCodeAuthenticationProvider oidcAuthorizationCodeAuthenticationProvider() {
|
||||
final OidcAuthorizationCodeAuthenticationProvider provider = new OidcAuthorizationCodeAuthenticationProvider(
|
||||
accessTokenResponseClient(),
|
||||
oidcUserService()
|
||||
);
|
||||
provider.setJwtDecoderFactory(idTokenDecoderFactory());
|
||||
return provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Access Token Response Client for retrieving Access Tokens using Authorization Codes
|
||||
*
|
||||
* @return OAuth2 Access Token Response Client
|
||||
*/
|
||||
@Bean
|
||||
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
|
||||
final DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
|
||||
accessTokenResponseClient.setRestOperations(oidcRestOperations());
|
||||
return accessTokenResponseClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenID Connect User Service wired to Authentication Provider for retrieving User Information
|
||||
*
|
||||
* @return OpenID Connect User Service
|
||||
*/
|
||||
@Bean
|
||||
public OidcUserService oidcUserService() {
|
||||
final OidcUserService oidcUserService = new OidcUserService();
|
||||
final DefaultOAuth2UserService userService = new DefaultOAuth2UserService();
|
||||
userService.setRestOperations(oidcRestOperations());
|
||||
oidcUserService.setOauth2UserService(userService);
|
||||
return oidcUserService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorized Client Repository for storing OpenID Connect Tokens in application State Manager
|
||||
*
|
||||
* @return Authorized Client Repository
|
||||
*/
|
||||
@Bean
|
||||
public StandardOidcAuthorizedClientRepository authorizedClientRepository() {
|
||||
final StateManager stateManager = stateManagerProvider.getStateManager(StandardOidcAuthorizedClientRepository.class.getName());
|
||||
return new StandardOidcAuthorizedClientRepository(stateManager, authorizedClientConverter());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthorizedClientExpirationCommand authorizedClientExpirationCommand() {
|
||||
final AuthorizedClientExpirationCommand command = new AuthorizedClientExpirationCommand(authorizedClientRepository(), tokenRevocationResponseClient());
|
||||
oidcCommandScheduler().scheduleAtFixedRate(command, keyRotationPeriod);
|
||||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Command Scheduled for OpenID Connect operations
|
||||
*
|
||||
* @return Thread Pool Task Executor
|
||||
*/
|
||||
@Bean
|
||||
public ThreadPoolTaskScheduler oidcCommandScheduler() {
|
||||
final ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
scheduler.setThreadNamePrefix(OidcSecurityConfiguration.class.getSimpleName());
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorized Client Converter for OpenID Connect Tokens supporting serialization of OpenID Connect Tokens
|
||||
*
|
||||
* @return Authorized Client Converter
|
||||
*/
|
||||
@Bean
|
||||
public AuthorizedClientConverter authorizedClientConverter() {
|
||||
return new StandardAuthorizedClientConverter(propertyEncryptor, clientRegistrationRepository());
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenID Connect Authorization Request Repository with Cache abstraction based on Caffeine implementation
|
||||
*
|
||||
* @return Authorization Request Repository
|
||||
*/
|
||||
@Bean
|
||||
public AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository() {
|
||||
final Cache<Object, Object> caffeineCache = Caffeine.newBuilder()
|
||||
.maximumSize(AUTHORIZATION_REQUEST_CACHE_SIZE)
|
||||
.expireAfterWrite(REQUEST_EXPIRATION)
|
||||
.build();
|
||||
final CaffeineCache cache = new CaffeineCache(StandardAuthorizationRequestRepository.class.getSimpleName(), caffeineCache);
|
||||
return new StandardAuthorizationRequestRepository(cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenID Connect Identifier Token Decoder with configured JWS Algorithm for verification
|
||||
*
|
||||
* @return OpenID Connect Identifier Token Decoder
|
||||
*/
|
||||
@Bean
|
||||
public JwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
|
||||
OidcIdTokenDecoderFactory idTokenDecoderFactory = new OidcIdTokenDecoderFactory();
|
||||
|
||||
final String preferredJwdAlgorithm = properties.getOidcPreferredJwsAlgorithm();
|
||||
if (StringUtils.isNotEmpty(preferredJwdAlgorithm)) {
|
||||
idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> getJwsAlgorithm(preferredJwdAlgorithm));
|
||||
}
|
||||
|
||||
return idTokenDecoderFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Token Revocation Response Client responsible for transmitting Refresh Token revocation requests to the Provider
|
||||
*
|
||||
* @return Token Revocation Response Client
|
||||
*/
|
||||
@Bean
|
||||
public TokenRevocationResponseClient tokenRevocationResponseClient() {
|
||||
return new StandardTokenRevocationResponseClient(oidcRestOperations(), clientRegistrationRepository());
|
||||
}
|
||||
|
||||
/**
|
||||
* Client Registration Repository for OpenID Connect Discovery
|
||||
*
|
||||
* @return Client Registration Repository
|
||||
*/
|
||||
@Bean
|
||||
public ClientRegistrationRepository clientRegistrationRepository() {
|
||||
final ClientRegistrationRepository clientRegistrationRepository;
|
||||
if (properties.isOidcEnabled()) {
|
||||
final ClientRegistrationProvider clientRegistrationProvider = new StandardClientRegistrationProvider(properties, oidcRestOperations());
|
||||
final ClientRegistration clientRegistration = clientRegistrationProvider.getClientRegistration();
|
||||
clientRegistrationRepository = new InMemoryClientRegistrationRepository(clientRegistration);
|
||||
} else {
|
||||
clientRegistrationRepository = new DisabledClientRegistrationRepository();
|
||||
}
|
||||
return clientRegistrationRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenID Connect REST Operations for communication with Authorization Servers
|
||||
*
|
||||
* @return REST Operations
|
||||
*/
|
||||
@Bean
|
||||
public RestOperations oidcRestOperations() {
|
||||
final RestTemplate restTemplate = new RestTemplate(oidcClientHttpRequestFactory());
|
||||
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
|
||||
restTemplate.setMessageConverters(
|
||||
Arrays.asList(
|
||||
new FormHttpMessageConverter(),
|
||||
new OAuth2AccessTokenResponseHttpMessageConverter(),
|
||||
new StringHttpMessageConverter(),
|
||||
new MappingJackson2HttpMessageConverter()
|
||||
)
|
||||
);
|
||||
return restTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenID Connect Client HTTP Request Factory for communication with Authorization Servers
|
||||
*
|
||||
* @return Client HTTP Request Factory
|
||||
*/
|
||||
@Bean
|
||||
public ClientHttpRequestFactory oidcClientHttpRequestFactory() {
|
||||
final OkHttpClient httpClient = getHttpClient();
|
||||
return new OkHttp3ClientHttpRequestFactory(httpClient);
|
||||
}
|
||||
|
||||
private OkHttpClient getHttpClient() {
|
||||
final Duration connectTimeout = getTimeout(properties.getOidcConnectTimeout());
|
||||
final Duration readTimeout = getTimeout(properties.getOidcReadTimeout());
|
||||
|
||||
final OkHttpClient.Builder builder = new OkHttpClient.Builder()
|
||||
.connectTimeout(connectTimeout)
|
||||
.readTimeout(readTimeout);
|
||||
|
||||
if (NIFI_TRUSTSTORE_STRATEGY.equals(properties.getOidcClientTruststoreStrategy())) {
|
||||
setSslSocketFactory(builder);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private Duration getTimeout(final String timeoutExpression) {
|
||||
try {
|
||||
final double duration = FormatUtils.getPreciseTimeDuration(timeoutExpression, TimeUnit.MILLISECONDS);
|
||||
final long rounded = Math.round(duration);
|
||||
return Duration.ofMillis(rounded);
|
||||
} catch (final RuntimeException e) {
|
||||
return DEFAULT_SOCKET_TIMEOUT;
|
||||
}
|
||||
}
|
||||
|
||||
private void setSslSocketFactory(final OkHttpClient.Builder builder) {
|
||||
final TlsConfiguration tlsConfiguration = StandardTlsConfiguration.fromNiFiProperties(properties);
|
||||
|
||||
try {
|
||||
final X509TrustManager trustManager = Objects.requireNonNull(SslContextFactory.getX509TrustManager(tlsConfiguration), "TrustManager required");
|
||||
final TrustManager[] trustManagers = new TrustManager[] { trustManager };
|
||||
final SSLContext sslContext = Objects.requireNonNull(SslContextFactory.createSslContext(tlsConfiguration, trustManagers), "SSLContext required");
|
||||
final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
|
||||
builder.sslSocketFactory(sslSocketFactory, trustManager);
|
||||
} catch (final TlsException e) {
|
||||
throw new OidcConfigurationException("OpenID Connect HTTP TLS configuration failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private OidcAuthenticationSuccessHandler getAuthenticationSuccessHandler() {
|
||||
final List<String> userClaimNames = new ArrayList<>();
|
||||
userClaimNames.add(properties.getOidcClaimIdentifyingUser());
|
||||
userClaimNames.addAll(properties.getOidcFallbackClaimsIdentifyingUser());
|
||||
|
||||
return new OidcAuthenticationSuccessHandler(
|
||||
bearerTokenProvider,
|
||||
idpUserGroupService,
|
||||
IdentityMappingUtil.getIdentityMappings(properties),
|
||||
IdentityMappingUtil.getGroupMappings(properties),
|
||||
userClaimNames,
|
||||
properties.getOidcClaimGroups()
|
||||
);
|
||||
}
|
||||
|
||||
private JwsAlgorithm getJwsAlgorithm(final String preferredJwsAlgorithm) {
|
||||
final JwsAlgorithm jwsAlgorithm;
|
||||
|
||||
final MacAlgorithm macAlgorithm = MacAlgorithm.from(preferredJwsAlgorithm);
|
||||
if (macAlgorithm == null) {
|
||||
final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.from(preferredJwsAlgorithm);
|
||||
if (signatureAlgorithm == null) {
|
||||
final String message = String.format("Preferred JWS Algorithm [%s] not supported", preferredJwsAlgorithm);
|
||||
throw new OidcConfigurationException(message);
|
||||
}
|
||||
jwsAlgorithm = signatureAlgorithm;
|
||||
} else {
|
||||
jwsAlgorithm = macAlgorithm;
|
||||
}
|
||||
|
||||
return jwsAlgorithm;
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.nifi.web;
|
||||
package org.apache.nifi.web.security.configuration;
|
||||
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.StandardAuthenticationEntryPoint;
|
||||
|
@ -24,21 +24,26 @@ import org.apache.nifi.web.security.csrf.SkipReplicatedCsrfFilter;
|
|||
import org.apache.nifi.web.security.csrf.StandardCookieCsrfTokenRepository;
|
||||
import org.apache.nifi.web.security.knox.KnoxAuthenticationFilter;
|
||||
import org.apache.nifi.web.security.log.AuthenticationUserFilter;
|
||||
import org.apache.nifi.web.security.oidc.OIDCEndpoints;
|
||||
import org.apache.nifi.web.security.oidc.client.web.OidcBearerTokenRefreshFilter;
|
||||
import org.apache.nifi.web.security.oidc.logout.OidcLogoutFilter;
|
||||
import org.apache.nifi.web.security.saml2.web.authentication.logout.Saml2LocalLogoutFilter;
|
||||
import org.apache.nifi.web.security.saml2.web.authentication.logout.Saml2SingleLogoutFilter;
|
||||
import org.apache.nifi.web.security.x509.X509AuthenticationFilter;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter;
|
||||
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
|
||||
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter;
|
||||
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter;
|
||||
import org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter;
|
||||
import org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseFilter;
|
||||
|
@ -53,10 +58,13 @@ import java.util.List;
|
|||
/**
|
||||
* Application Security Configuration using Spring Security
|
||||
*/
|
||||
@Import({
|
||||
AuthenticationSecurityConfiguration.class
|
||||
})
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||
public class NiFiWebApiSecurityConfiguration {
|
||||
@EnableMethodSecurity
|
||||
public class WebSecurityConfiguration {
|
||||
/**
|
||||
* Spring Security Authentication Manager configured using Authentication Providers from specific configuration classes
|
||||
*
|
||||
|
@ -77,6 +85,11 @@ public class NiFiWebApiSecurityConfiguration {
|
|||
final BearerTokenAuthenticationFilter bearerTokenAuthenticationFilter,
|
||||
final KnoxAuthenticationFilter knoxAuthenticationFilter,
|
||||
final NiFiAnonymousAuthenticationFilter anonymousAuthenticationFilter,
|
||||
final OAuth2LoginAuthenticationFilter oAuth2LoginAuthenticationFilter,
|
||||
final OAuth2AuthorizationCodeGrantFilter oAuth2AuthorizationCodeGrantFilter,
|
||||
final OAuth2AuthorizationRequestRedirectFilter oAuth2AuthorizationRequestRedirectFilter,
|
||||
final OidcBearerTokenRefreshFilter oidcBearerTokenRefreshFilter,
|
||||
final OidcLogoutFilter oidcLogoutFilter,
|
||||
final Saml2WebSsoAuthenticationFilter saml2WebSsoAuthenticationFilter,
|
||||
final Saml2WebSsoAuthenticationRequestFilter saml2WebSsoAuthenticationRequestFilter,
|
||||
final Saml2MetadataFilter saml2MetadataFilter,
|
||||
|
@ -95,18 +108,14 @@ public class NiFiWebApiSecurityConfiguration {
|
|||
.servletApi().disable()
|
||||
.securityContext().disable()
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
.antMatchers(
|
||||
.requestMatchers(
|
||||
"/access",
|
||||
"/access/config",
|
||||
"/access/token",
|
||||
"/access/kerberos",
|
||||
"/access/knox/callback",
|
||||
"/access/knox/request",
|
||||
"/access/logout/complete",
|
||||
OIDCEndpoints.TOKEN_EXCHANGE,
|
||||
OIDCEndpoints.LOGIN_REQUEST,
|
||||
OIDCEndpoints.LOGIN_CALLBACK,
|
||||
OIDCEndpoints.LOGOUT_CALLBACK
|
||||
"/access/logout/complete"
|
||||
).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
|
@ -149,6 +158,14 @@ public class NiFiWebApiSecurityConfiguration {
|
|||
}
|
||||
}
|
||||
|
||||
if (properties.isOidcEnabled()) {
|
||||
http.addFilterBefore(oAuth2LoginAuthenticationFilter, AnonymousAuthenticationFilter.class);
|
||||
http.addFilterBefore(oAuth2AuthorizationCodeGrantFilter, AnonymousAuthenticationFilter.class);
|
||||
http.addFilterBefore(oAuth2AuthorizationRequestRedirectFilter, AnonymousAuthenticationFilter.class);
|
||||
http.addFilterBefore(oidcBearerTokenRefreshFilter, AnonymousAuthenticationFilter.class);
|
||||
http.addFilterBefore(oidcLogoutFilter, CsrfFilter.class);
|
||||
}
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.logout;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Standard Logout Filter completes application Logout Requests
|
||||
*/
|
||||
public class StandardLogoutFilter extends OncePerRequestFilter {
|
||||
private final AntPathRequestMatcher requestMatcher;
|
||||
|
||||
private final LogoutSuccessHandler logoutSuccessHandler;
|
||||
|
||||
public StandardLogoutFilter(
|
||||
final AntPathRequestMatcher requestMatcher,
|
||||
final LogoutSuccessHandler logoutSuccessHandler
|
||||
) {
|
||||
this.requestMatcher = requestMatcher;
|
||||
this.logoutSuccessHandler = logoutSuccessHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call Logout Success Handler when request path matches
|
||||
*
|
||||
* @param request HTTP Servlet Request
|
||||
* @param response HTTP Servlet Response
|
||||
* @param filterChain Filter Chain
|
||||
* @throws ServletException Thrown on FilterChain.doFilter() failures
|
||||
* @throws IOException Thrown on FilterChain.doFilter() failures
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
|
||||
if (requestMatcher.matches(request)) {
|
||||
final SecurityContext securityContext = SecurityContextHolder.getContext();
|
||||
final Authentication authentication = securityContext.getAuthentication();
|
||||
logoutSuccessHandler.onLogoutSuccess(request, response, authentication);
|
||||
} else {
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,22 +16,16 @@
|
|||
*/
|
||||
package org.apache.nifi.web.security.oidc;
|
||||
|
||||
public interface OIDCEndpoints {
|
||||
/**
|
||||
* OpenID Connect Configuration Exception
|
||||
*/
|
||||
public class OidcConfigurationException extends RuntimeException {
|
||||
|
||||
String OIDC_ACCESS_ROOT = "/access/oidc";
|
||||
public OidcConfigurationException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
String LOGIN_REQUEST_RELATIVE = "/request";
|
||||
String LOGIN_REQUEST = OIDC_ACCESS_ROOT + LOGIN_REQUEST_RELATIVE;
|
||||
|
||||
String LOGIN_CALLBACK_RELATIVE = "/callback";
|
||||
String LOGIN_CALLBACK = OIDC_ACCESS_ROOT + LOGIN_CALLBACK_RELATIVE;
|
||||
|
||||
String TOKEN_EXCHANGE_RELATIVE = "/exchange";
|
||||
String TOKEN_EXCHANGE = OIDC_ACCESS_ROOT + TOKEN_EXCHANGE_RELATIVE;
|
||||
|
||||
String LOGOUT_REQUEST_RELATIVE = "/logout";
|
||||
String LOGOUT_REQUEST = OIDC_ACCESS_ROOT + LOGOUT_REQUEST_RELATIVE;
|
||||
|
||||
String LOGOUT_CALLBACK_RELATIVE = "/logoutCallback";
|
||||
String LOGOUT_CALLBACK = OIDC_ACCESS_ROOT + LOGOUT_CALLBACK_RELATIVE;
|
||||
public OidcConfigurationException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc;
|
||||
|
||||
|
||||
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
|
||||
import com.nimbusds.oauth2.sdk.Scope;
|
||||
import com.nimbusds.oauth2.sdk.id.ClientID;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||
|
||||
public interface OidcIdentityProvider {
|
||||
|
||||
String OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED = "OpenId Connect support is not configured";
|
||||
|
||||
/**
|
||||
* Initializes the provider.
|
||||
*/
|
||||
void initializeProvider();
|
||||
|
||||
/**
|
||||
* Returns whether OIDC support is enabled.
|
||||
*
|
||||
* @return whether OIDC support is enabled
|
||||
*/
|
||||
boolean isOidcEnabled();
|
||||
|
||||
/**
|
||||
* Returns the configured client id.
|
||||
*
|
||||
* @return the client id
|
||||
*/
|
||||
ClientID getClientId();
|
||||
|
||||
/**
|
||||
* Returns the URI for the authorization endpoint.
|
||||
*
|
||||
* @return uri for the authorization endpoint
|
||||
*/
|
||||
URI getAuthorizationEndpoint();
|
||||
|
||||
/**
|
||||
* Returns the URI for the end session endpoint.
|
||||
*
|
||||
* @return uri for the end session endpoint
|
||||
*/
|
||||
URI getEndSessionEndpoint();
|
||||
|
||||
/**
|
||||
* Returns the URI for the revocation endpoint.
|
||||
*
|
||||
* @return uri for the revocation endpoint
|
||||
*/
|
||||
URI getRevocationEndpoint();
|
||||
|
||||
/**
|
||||
* Returns the scopes supported by the OIDC provider.
|
||||
*
|
||||
* @return support scopes
|
||||
*/
|
||||
Scope getScope();
|
||||
|
||||
/**
|
||||
* Exchanges the supplied authorization grant for a Login ID Token. Extracts the identity from the ID
|
||||
* token.
|
||||
*
|
||||
* @param authorizationGrant authorization grant for invoking the Token Endpoint
|
||||
* @return a Login Authentication Token
|
||||
* @throws IOException if there was an exceptional error while communicating with the OIDC provider
|
||||
*/
|
||||
LoginAuthenticationToken exchangeAuthorizationCodeforLoginAuthenticationToken(AuthorizationGrant authorizationGrant) throws IOException;
|
||||
|
||||
/**
|
||||
* Exchanges the supplied authorization grant for an Access Token.
|
||||
*
|
||||
* @param authorizationGrant authorization grant for invoking the Token Endpoint
|
||||
* @return an Access Token String
|
||||
* @throws Exception if there was an exceptional error while communicating with the OIDC provider
|
||||
*/
|
||||
String exchangeAuthorizationCodeForAccessToken(AuthorizationGrant authorizationGrant) throws Exception;
|
||||
|
||||
/**
|
||||
* Exchanges the supplied authorization grant for an ID Token.
|
||||
*
|
||||
* @param authorizationGrant authorization grant for invoking the Token Endpoint
|
||||
* @return an ID Token String
|
||||
* @throws IOException if there was an exceptional error while communicating with the OIDC provider
|
||||
*/
|
||||
String exchangeAuthorizationCodeForIdToken(final AuthorizationGrant authorizationGrant) throws IOException;
|
||||
}
|
|
@ -1,270 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
|
||||
import com.nimbusds.oauth2.sdk.Scope;
|
||||
import com.nimbusds.oauth2.sdk.id.State;
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||
import org.apache.nifi.web.security.util.CacheKey;
|
||||
import org.apache.nifi.web.security.util.IdentityProviderUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.apache.nifi.web.security.oidc.StandardOidcIdentityProvider.OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED;
|
||||
|
||||
/**
|
||||
* OidcService is a service for managing the OpenId Connect Authorization flow.
|
||||
*/
|
||||
public class OidcService {
|
||||
|
||||
private final OidcIdentityProvider identityProvider;
|
||||
private final Cache<CacheKey, State> stateLookupForPendingRequests; // identifier from cookie -> state value
|
||||
private final Cache<CacheKey, String> jwtLookupForCompletedRequests; // identifier from cookie -> jwt or identity (and generate jwt on retrieval)
|
||||
|
||||
/**
|
||||
* Creates a new OIDC with an expiration of 1 minute.
|
||||
*
|
||||
* @param identityProvider The identity provider
|
||||
*/
|
||||
public OidcService(final OidcIdentityProvider identityProvider) {
|
||||
this(identityProvider, 60, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new OIDC Service.
|
||||
*
|
||||
* @param identityProvider The identity provider
|
||||
* @param duration The expiration duration
|
||||
* @param units The expiration units
|
||||
* @throws NullPointerException If units is null
|
||||
* @throws IllegalArgumentException If duration is negative
|
||||
*/
|
||||
public OidcService(final OidcIdentityProvider identityProvider, final int duration, final TimeUnit units) {
|
||||
if (identityProvider == null) {
|
||||
throw new RuntimeException("The OidcIdentityProvider must be specified.");
|
||||
}
|
||||
|
||||
identityProvider.initializeProvider();
|
||||
this.identityProvider = identityProvider;
|
||||
this.stateLookupForPendingRequests = Caffeine.newBuilder().expireAfterWrite(duration, units).build();
|
||||
this.jwtLookupForCompletedRequests = Caffeine.newBuilder().expireAfterWrite(duration, units).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether OpenId Connect is enabled.
|
||||
*
|
||||
* @return whether OpenId Connect is enabled
|
||||
*/
|
||||
public boolean isOidcEnabled() {
|
||||
return identityProvider.isOidcEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OpenId Connect authorization endpoint.
|
||||
*
|
||||
* @return the authorization endpoint
|
||||
*/
|
||||
public URI getAuthorizationEndpoint() {
|
||||
return identityProvider.getAuthorizationEndpoint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OpenId Connect end session endpoint.
|
||||
*
|
||||
* @return the end session endpoint
|
||||
*/
|
||||
public URI getEndSessionEndpoint() {
|
||||
return identityProvider.getEndSessionEndpoint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OpenId Connect revocation endpoint.
|
||||
*
|
||||
* @return the revocation endpoint
|
||||
*/
|
||||
public URI getRevocationEndpoint() {
|
||||
return identityProvider.getRevocationEndpoint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OpenId Connect scope.
|
||||
*
|
||||
* @return scope
|
||||
*/
|
||||
public Scope getScope() {
|
||||
return identityProvider.getScope();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OpenId Connect client id.
|
||||
*
|
||||
* @return client id
|
||||
*/
|
||||
public String getClientId() {
|
||||
return identityProvider.getClientId().getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates an OpenId Connection authorization code flow using the specified request identifier to maintain state.
|
||||
*
|
||||
* @param oidcRequestIdentifier request identifier
|
||||
* @return state
|
||||
*/
|
||||
public State createState(final String oidcRequestIdentifier) {
|
||||
if (!isOidcEnabled()) {
|
||||
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
||||
}
|
||||
|
||||
final CacheKey oidcRequestIdentifierKey = new CacheKey(oidcRequestIdentifier);
|
||||
final State state = new State(IdentityProviderUtils.generateStateValue());
|
||||
|
||||
synchronized (stateLookupForPendingRequests) {
|
||||
final State cachedState = stateLookupForPendingRequests.get(oidcRequestIdentifierKey, key -> state);
|
||||
if (!IdentityProviderUtils.timeConstantEqualityCheck(state.getValue(), cachedState.getValue())) {
|
||||
throw new IllegalStateException("An existing login request is already in progress.");
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the proposed state with the given request identifier. Will return false if the
|
||||
* state does not match or if entry for this request identifier has expired.
|
||||
*
|
||||
* @param oidcRequestIdentifier request identifier
|
||||
* @param proposedState proposed state
|
||||
* @return whether the state is valid or not
|
||||
*/
|
||||
public boolean isStateValid(final String oidcRequestIdentifier, final State proposedState) {
|
||||
if (!isOidcEnabled()) {
|
||||
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
||||
}
|
||||
|
||||
if (proposedState == null) {
|
||||
throw new IllegalArgumentException("Proposed state must be specified.");
|
||||
}
|
||||
|
||||
final CacheKey oidcRequestIdentifierKey = new CacheKey(oidcRequestIdentifier);
|
||||
|
||||
synchronized (stateLookupForPendingRequests) {
|
||||
final State state = stateLookupForPendingRequests.getIfPresent(oidcRequestIdentifierKey);
|
||||
if (state != null) {
|
||||
stateLookupForPendingRequests.invalidate(oidcRequestIdentifierKey);
|
||||
}
|
||||
|
||||
return state != null && IdentityProviderUtils.timeConstantEqualityCheck(state.getValue(), proposedState.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchanges the specified authorization grant for an ID token.
|
||||
*
|
||||
* @param authorizationGrant authorization grant
|
||||
* @return a Login Authentication Token
|
||||
* @throws IOException exceptional case for communication error with the OpenId Connect provider
|
||||
*/
|
||||
public LoginAuthenticationToken exchangeAuthorizationCodeForLoginAuthenticationToken(final AuthorizationGrant authorizationGrant) throws IOException {
|
||||
if (!isOidcEnabled()) {
|
||||
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
||||
}
|
||||
|
||||
// Retrieve Login Authentication Token
|
||||
return identityProvider.exchangeAuthorizationCodeforLoginAuthenticationToken(authorizationGrant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchanges the specified authorization grant for an access token.
|
||||
*
|
||||
* @param authorizationGrant authorization grant
|
||||
* @return an Access Token string
|
||||
* @throws IOException exceptional case for communication error with the OpenId Connect provider
|
||||
*/
|
||||
public String exchangeAuthorizationCodeForAccessToken(final AuthorizationGrant authorizationGrant) throws Exception {
|
||||
if (!isOidcEnabled()) {
|
||||
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
||||
}
|
||||
|
||||
// Retrieve access token
|
||||
return identityProvider.exchangeAuthorizationCodeForAccessToken(authorizationGrant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchanges the specified authorization grant for an ID Token.
|
||||
*
|
||||
* @param authorizationGrant authorization grant
|
||||
* @return an ID Token string
|
||||
* @throws IOException exceptional case for communication error with the OpenId Connect provider
|
||||
*/
|
||||
public String exchangeAuthorizationCodeForIdToken(final AuthorizationGrant authorizationGrant) throws IOException {
|
||||
if (!isOidcEnabled()) {
|
||||
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
||||
}
|
||||
|
||||
// Retrieve ID token
|
||||
return identityProvider.exchangeAuthorizationCodeForIdToken(authorizationGrant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the NiFi Jwt.
|
||||
*
|
||||
* @param oidcRequestIdentifier request identifier
|
||||
* @param jwt NiFi JWT
|
||||
*/
|
||||
public void storeJwt(final String oidcRequestIdentifier, final String jwt) {
|
||||
final CacheKey oidcRequestIdentifierKey = new CacheKey(oidcRequestIdentifier);
|
||||
|
||||
// Cache the jwt for later retrieval
|
||||
synchronized (jwtLookupForCompletedRequests) {
|
||||
final String cachedJwt = jwtLookupForCompletedRequests.get(oidcRequestIdentifierKey, key -> jwt);
|
||||
if (!IdentityProviderUtils.timeConstantEqualityCheck(jwt, cachedJwt)) {
|
||||
throw new IllegalStateException("An existing login request is already in progress.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the resulting JWT for the given request identifier. Will return null if the request
|
||||
* identifier is not associated with a JWT or if the login sequence was not completed before
|
||||
* this request identifier expired.
|
||||
*
|
||||
* @param oidcRequestIdentifier request identifier
|
||||
* @return jwt token
|
||||
*/
|
||||
public String getJwt(final String oidcRequestIdentifier) {
|
||||
if (!isOidcEnabled()) {
|
||||
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
||||
}
|
||||
|
||||
final CacheKey oidcRequestIdentifierKey = new CacheKey(oidcRequestIdentifier);
|
||||
|
||||
synchronized (jwtLookupForCompletedRequests) {
|
||||
final String jwt = jwtLookupForCompletedRequests.getIfPresent(oidcRequestIdentifierKey);
|
||||
if (jwt != null) {
|
||||
jwtLookupForCompletedRequests.invalidate(oidcRequestIdentifierKey);
|
||||
}
|
||||
|
||||
return jwt;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -17,17 +17,20 @@
|
|||
package org.apache.nifi.web.security.oidc;
|
||||
|
||||
/**
|
||||
* Indicates which truststore should be used when creating an HttpClient for an https URL.
|
||||
* Shared configuration for OpenID Connect URL Paths
|
||||
*/
|
||||
public enum TruststoreStrategy {
|
||||
public enum OidcUrlPath {
|
||||
CALLBACK("/access/oidc/callback"),
|
||||
|
||||
/**
|
||||
* Use the JDK truststore.
|
||||
*/
|
||||
JDK,
|
||||
LOGOUT("/access/oidc/logout");
|
||||
|
||||
/**
|
||||
* Use NiFi's truststore specified in nifi.properties.
|
||||
*/
|
||||
NIFI;
|
||||
private final String path;
|
||||
|
||||
OidcUrlPath(final String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
}
|
|
@ -1,631 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc;
|
||||
|
||||
import com.nimbusds.jose.JOSEException;
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.proc.BadJOSEException;
|
||||
import com.nimbusds.jose.util.DefaultResourceRetriever;
|
||||
import com.nimbusds.jose.util.ResourceRetriever;
|
||||
import com.nimbusds.jwt.JWT;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
|
||||
import com.nimbusds.oauth2.sdk.ErrorObject;
|
||||
import com.nimbusds.oauth2.sdk.ParseException;
|
||||
import com.nimbusds.oauth2.sdk.Request;
|
||||
import com.nimbusds.oauth2.sdk.Scope;
|
||||
import com.nimbusds.oauth2.sdk.TokenErrorResponse;
|
||||
import com.nimbusds.oauth2.sdk.TokenRequest;
|
||||
import com.nimbusds.oauth2.sdk.TokenResponse;
|
||||
import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
|
||||
import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
|
||||
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
|
||||
import com.nimbusds.oauth2.sdk.auth.ClientSecretPost;
|
||||
import com.nimbusds.oauth2.sdk.auth.Secret;
|
||||
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
|
||||
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
|
||||
import com.nimbusds.oauth2.sdk.id.ClientID;
|
||||
import com.nimbusds.oauth2.sdk.token.AccessToken;
|
||||
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
|
||||
import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
|
||||
import com.nimbusds.openid.connect.sdk.OIDCTokenResponseParser;
|
||||
import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
|
||||
import com.nimbusds.openid.connect.sdk.UserInfoRequest;
|
||||
import com.nimbusds.openid.connect.sdk.UserInfoResponse;
|
||||
import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
|
||||
import com.nimbusds.openid.connect.sdk.claims.AccessTokenHash;
|
||||
import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
|
||||
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
|
||||
import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
|
||||
import com.nimbusds.openid.connect.sdk.validators.AccessTokenValidator;
|
||||
import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator;
|
||||
import com.nimbusds.openid.connect.sdk.validators.InvalidHashException;
|
||||
import net.minidev.json.JSONObject;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.nifi.authentication.exception.IdentityAccessException;
|
||||
import org.apache.nifi.security.util.SslContextFactory;
|
||||
import org.apache.nifi.security.util.StandardTlsConfiguration;
|
||||
import org.apache.nifi.security.util.TlsConfiguration;
|
||||
import org.apache.nifi.security.util.TlsException;
|
||||
import org.apache.nifi.util.FormatUtils;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
/**
|
||||
* OidcProvider for managing the OpenId Connect Authorization flow.
|
||||
*/
|
||||
public class StandardOidcIdentityProvider implements OidcIdentityProvider {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(StandardOidcIdentityProvider.class);
|
||||
private final String EMAIL_CLAIM = "email";
|
||||
|
||||
private final NiFiProperties properties;
|
||||
|
||||
private OIDCProviderMetadata oidcProviderMetadata;
|
||||
private int oidcConnectTimeout;
|
||||
private int oidcReadTimeout;
|
||||
private IDTokenValidator tokenValidator;
|
||||
private ClientID clientId;
|
||||
private Secret clientSecret;
|
||||
private SSLContext sslContext;
|
||||
|
||||
/**
|
||||
* Creates a new StandardOidcIdentityProvider.
|
||||
*
|
||||
* @param properties properties
|
||||
*/
|
||||
public StandardOidcIdentityProvider(final NiFiProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads OIDC configuration values from {@link NiFiProperties}, connects to external OIDC provider, and retrieves
|
||||
* and validates provider metadata.
|
||||
*/
|
||||
@Override
|
||||
public void initializeProvider() {
|
||||
// attempt to process the oidc configuration if configured
|
||||
if (!properties.isOidcEnabled()) {
|
||||
logger.debug("The OIDC provider is not configured or enabled");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up trust store SSLContext
|
||||
if (TruststoreStrategy.NIFI.name().equals(properties.getOidcClientTruststoreStrategy())) {
|
||||
setSslContext();
|
||||
}
|
||||
|
||||
validateOIDCConfiguration();
|
||||
|
||||
try {
|
||||
// retrieve the oidc provider metadata
|
||||
oidcProviderMetadata = retrieveOidcProviderMetadata(properties.getOidcDiscoveryUrl());
|
||||
} catch (IOException | ParseException e) {
|
||||
throw new RuntimeException("Unable to retrieve OpenId Connect Provider metadata from: " + properties.getOidcDiscoveryUrl(), e);
|
||||
}
|
||||
|
||||
validateOIDCProviderMetadata();
|
||||
}
|
||||
|
||||
private void setSslContext() {
|
||||
TlsConfiguration tlsConfiguration = StandardTlsConfiguration.fromNiFiProperties(properties);
|
||||
try {
|
||||
this.sslContext = SslContextFactory.createSslContext(tlsConfiguration);
|
||||
} catch (TlsException e) {
|
||||
throw new RuntimeException("Unable to establish an SSL context for OIDC identity provider from nifi.properties", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the retrieved OIDC provider metadata.
|
||||
*/
|
||||
private void validateOIDCProviderMetadata() {
|
||||
// ensure the authorization endpoint is present
|
||||
if (oidcProviderMetadata.getAuthorizationEndpointURI() == null) {
|
||||
throw new RuntimeException("OpenId Connect Provider metadata does not contain an Authorization Endpoint.");
|
||||
}
|
||||
|
||||
// ensure the token endpoint is present
|
||||
if (oidcProviderMetadata.getTokenEndpointURI() == null) {
|
||||
throw new RuntimeException("OpenId Connect Provider metadata does not contain a Token Endpoint.");
|
||||
}
|
||||
|
||||
// ensure the oidc provider supports basic or post client auth
|
||||
List<ClientAuthenticationMethod> clientAuthenticationMethods = oidcProviderMetadata.getTokenEndpointAuthMethods();
|
||||
logger.info("OpenId Connect: Available clientAuthenticationMethods {} ", clientAuthenticationMethods);
|
||||
if (clientAuthenticationMethods == null || clientAuthenticationMethods.isEmpty()) {
|
||||
clientAuthenticationMethods = new ArrayList<>();
|
||||
clientAuthenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
|
||||
oidcProviderMetadata.setTokenEndpointAuthMethods(clientAuthenticationMethods);
|
||||
logger.warn("OpenId Connect: ClientAuthenticationMethods is null, Setting clientAuthenticationMethods as CLIENT_SECRET_BASIC");
|
||||
} else if (!clientAuthenticationMethods.contains(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
&& !clientAuthenticationMethods.contains(ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
|
||||
throw new RuntimeException(String.format("OpenId Connect Provider does not support %s or %s",
|
||||
ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue(),
|
||||
ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()));
|
||||
}
|
||||
|
||||
// extract the supported json web signature algorithms
|
||||
final List<JWSAlgorithm> allowedAlgorithms = oidcProviderMetadata.getIDTokenJWSAlgs();
|
||||
if (allowedAlgorithms == null || allowedAlgorithms.isEmpty()) {
|
||||
throw new RuntimeException("The OpenId Connect Provider does not support any JWS algorithms.");
|
||||
}
|
||||
|
||||
try {
|
||||
// get the preferred json web signature algorithm
|
||||
final JWSAlgorithm preferredJwsAlgorithm = extractJwsAlgorithm();
|
||||
|
||||
if (preferredJwsAlgorithm == null) {
|
||||
tokenValidator = new IDTokenValidator(oidcProviderMetadata.getIssuer(), clientId);
|
||||
} else if (JWSAlgorithm.HS256.equals(preferredJwsAlgorithm) || JWSAlgorithm.HS384.equals(preferredJwsAlgorithm) || JWSAlgorithm.HS512.equals(preferredJwsAlgorithm)) {
|
||||
tokenValidator = new IDTokenValidator(oidcProviderMetadata.getIssuer(), clientId, preferredJwsAlgorithm, clientSecret);
|
||||
} else {
|
||||
final ResourceRetriever retriever = getResourceRetriever();
|
||||
tokenValidator = new IDTokenValidator(oidcProviderMetadata.getIssuer(), clientId, preferredJwsAlgorithm, oidcProviderMetadata.getJWKSetURI().toURL(), retriever);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
throw new RuntimeException("Unable to create the ID token validator for the configured OpenId Connect Provider: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private JWSAlgorithm extractJwsAlgorithm() {
|
||||
|
||||
final String rawPreferredJwsAlgorithm = properties.getOidcPreferredJwsAlgorithm();
|
||||
|
||||
final JWSAlgorithm preferredJwsAlgorithm;
|
||||
if (StringUtils.isBlank(rawPreferredJwsAlgorithm)) {
|
||||
preferredJwsAlgorithm = JWSAlgorithm.RS256;
|
||||
} else {
|
||||
if ("none".equalsIgnoreCase(rawPreferredJwsAlgorithm)) {
|
||||
preferredJwsAlgorithm = null;
|
||||
} else {
|
||||
preferredJwsAlgorithm = JWSAlgorithm.parse(rawPreferredJwsAlgorithm);
|
||||
}
|
||||
}
|
||||
return preferredJwsAlgorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the initial configuration values relating to the OIDC provider from the class {@link NiFiProperties} and populates the individual fields.
|
||||
*/
|
||||
private void validateOIDCConfiguration() {
|
||||
if (properties.isLoginIdentityProviderEnabled() || properties.isKnoxSsoEnabled()) {
|
||||
throw new RuntimeException("OpenId Connect support cannot be enabled if the Login Identity Provider or Apache Knox SSO is configured.");
|
||||
}
|
||||
|
||||
// oidc connect timeout
|
||||
final String rawConnectTimeout = properties.getOidcConnectTimeout();
|
||||
try {
|
||||
oidcConnectTimeout = (int) FormatUtils.getPreciseTimeDuration(rawConnectTimeout, TimeUnit.MILLISECONDS);
|
||||
} catch (final Exception e) {
|
||||
logger.warn("Failed to parse value of property '{}' as a valid time period. Value was '{}'. Ignoring this value and using the default value of '{}'",
|
||||
NiFiProperties.SECURITY_USER_OIDC_CONNECT_TIMEOUT, rawConnectTimeout, NiFiProperties.DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT);
|
||||
oidcConnectTimeout = (int) FormatUtils.getPreciseTimeDuration(NiFiProperties.DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
// oidc read timeout
|
||||
final String rawReadTimeout = properties.getOidcReadTimeout();
|
||||
try {
|
||||
oidcReadTimeout = (int) FormatUtils.getPreciseTimeDuration(rawReadTimeout, TimeUnit.MILLISECONDS);
|
||||
} catch (final Exception e) {
|
||||
logger.warn("Failed to parse value of property '{}' as a valid time period. Value was '{}'. Ignoring this value and using the default value of '{}'",
|
||||
NiFiProperties.SECURITY_USER_OIDC_READ_TIMEOUT, rawReadTimeout, NiFiProperties.DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT);
|
||||
oidcReadTimeout = (int) FormatUtils.getPreciseTimeDuration(NiFiProperties.DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
// client id
|
||||
final String rawClientId = properties.getOidcClientId();
|
||||
if (StringUtils.isBlank(rawClientId)) {
|
||||
throw new RuntimeException("Client ID is required when configuring an OIDC Provider.");
|
||||
}
|
||||
clientId = new ClientID(rawClientId);
|
||||
|
||||
// client secret
|
||||
final String rawClientSecret = properties.getOidcClientSecret();
|
||||
if (StringUtils.isBlank(rawClientSecret)) {
|
||||
throw new RuntimeException("Client secret is required when configuring an OIDC Provider.");
|
||||
}
|
||||
clientSecret = new Secret(rawClientSecret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the retrieved OIDC provider metadata from the external provider.
|
||||
*
|
||||
* @param discoveryUri the remote OIDC provider endpoint for service discovery
|
||||
* @return the provider metadata
|
||||
* @throws IOException if there is a problem connecting to the remote endpoint
|
||||
* @throws ParseException if there is a problem parsing the response
|
||||
*/
|
||||
private OIDCProviderMetadata retrieveOidcProviderMetadata(final String discoveryUri) throws IOException, ParseException {
|
||||
final URL url = new URL(discoveryUri);
|
||||
final HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.GET, url);
|
||||
setHttpRequestProperties(httpRequest);
|
||||
final HTTPResponse httpResponse = httpRequest.send();
|
||||
|
||||
if (httpResponse.getStatusCode() != 200) {
|
||||
throw new IOException("Unable to download OpenId Connect Provider metadata from " + url + ": Status code " + httpResponse.getStatusCode());
|
||||
}
|
||||
|
||||
final JSONObject jsonObject = httpResponse.getContentAsJSONObject();
|
||||
return OIDCProviderMetadata.parse(jsonObject);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOidcEnabled() {
|
||||
return properties.isOidcEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getAuthorizationEndpoint() {
|
||||
if (!isOidcEnabled()) {
|
||||
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
||||
}
|
||||
return oidcProviderMetadata.getAuthorizationEndpointURI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getEndSessionEndpoint() {
|
||||
if (!isOidcEnabled()) {
|
||||
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
||||
}
|
||||
return oidcProviderMetadata.getEndSessionEndpointURI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getRevocationEndpoint() {
|
||||
if (!isOidcEnabled()) {
|
||||
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
||||
}
|
||||
return oidcProviderMetadata.getRevocationEndpointURI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Scope getScope() {
|
||||
if (!isOidcEnabled()) {
|
||||
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
||||
}
|
||||
|
||||
Scope scope = new Scope("openid", EMAIL_CLAIM);
|
||||
|
||||
for (String additionalScope : properties.getOidcAdditionalScopes()) {
|
||||
// Scope automatically prevents duplicated entries
|
||||
scope.add(additionalScope);
|
||||
}
|
||||
|
||||
return scope;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientID getClientId() {
|
||||
if (!isOidcEnabled()) {
|
||||
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
||||
}
|
||||
return clientId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginAuthenticationToken exchangeAuthorizationCodeforLoginAuthenticationToken(final AuthorizationGrant authorizationGrant) throws IOException {
|
||||
// Check if OIDC is enabled
|
||||
if (!isOidcEnabled()) {
|
||||
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
||||
}
|
||||
|
||||
try {
|
||||
// Authenticate and authorize the client request
|
||||
final TokenResponse response = authorizeClient(authorizationGrant);
|
||||
|
||||
// Convert response to Login Authentication Token
|
||||
// We only want to do this for login
|
||||
return convertOIDCTokenToLoginAuthenticationToken((OIDCTokenResponse) response);
|
||||
|
||||
} catch (final RuntimeException | ParseException | JOSEException | BadJOSEException | java.text.ParseException e) {
|
||||
throw new RuntimeException("Unable to parse the response from the Token request: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String exchangeAuthorizationCodeForAccessToken(final AuthorizationGrant authorizationGrant) throws Exception {
|
||||
// Check if OIDC is enabled
|
||||
if (!isOidcEnabled()) {
|
||||
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
||||
}
|
||||
|
||||
try {
|
||||
// Authenticate and authorize the client request
|
||||
final TokenResponse response = authorizeClient(authorizationGrant);
|
||||
return getAccessTokenString((OIDCTokenResponse) response);
|
||||
|
||||
} catch (final RuntimeException | ParseException | IOException | java.text.ParseException | InvalidHashException e) {
|
||||
throw new RuntimeException("Unable to parse the response from the Token request: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String exchangeAuthorizationCodeForIdToken(final AuthorizationGrant authorizationGrant) {
|
||||
// Check if OIDC is enabled
|
||||
if (!isOidcEnabled()) {
|
||||
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
||||
}
|
||||
|
||||
try {
|
||||
// Authenticate and authorize the client request
|
||||
final TokenResponse response = authorizeClient(authorizationGrant);
|
||||
return getIdTokenString((OIDCTokenResponse) response);
|
||||
|
||||
} catch (final RuntimeException | JOSEException | BadJOSEException | ParseException | IOException e) {
|
||||
throw new RuntimeException("Unable to parse the response from the Token request: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getAccessTokenString(final OIDCTokenResponse response) throws Exception {
|
||||
final OIDCTokens oidcTokens = getOidcTokens(response);
|
||||
|
||||
// Validate the Access Token
|
||||
validateAccessToken(oidcTokens);
|
||||
|
||||
// Return the Access Token String
|
||||
return oidcTokens.getAccessToken().getValue();
|
||||
}
|
||||
|
||||
private String getIdTokenString(OIDCTokenResponse response) throws BadJOSEException, JOSEException {
|
||||
final OIDCTokens oidcTokens = getOidcTokens(response);
|
||||
|
||||
// Validate the Token - no nonce required for authorization code flow
|
||||
validateIdToken(oidcTokens.getIDToken());
|
||||
|
||||
// Return the ID Token string
|
||||
return oidcTokens.getIDTokenString();
|
||||
}
|
||||
|
||||
private TokenResponse authorizeClient(AuthorizationGrant authorizationGrant) throws ParseException, IOException {
|
||||
// Build ClientAuthentication
|
||||
final ClientAuthentication clientAuthentication = createClientAuthentication();
|
||||
|
||||
// Build the token request
|
||||
final HTTPRequest tokenHttpRequest = createTokenHTTPRequest(authorizationGrant, clientAuthentication);
|
||||
|
||||
// Send the request and parse for success
|
||||
return authorizeClientRequest(tokenHttpRequest);
|
||||
}
|
||||
|
||||
private TokenResponse authorizeClientRequest(HTTPRequest tokenHttpRequest) throws ParseException, IOException {
|
||||
// Get the token response
|
||||
final TokenResponse response = OIDCTokenResponseParser.parse(tokenHttpRequest.send());
|
||||
|
||||
// Handle success
|
||||
if (response.indicatesSuccess()) {
|
||||
return response;
|
||||
} else {
|
||||
// If the response was not successful
|
||||
final TokenErrorResponse errorResponse = (TokenErrorResponse) response;
|
||||
final ErrorObject errorObject = errorResponse.getErrorObject();
|
||||
throw new RuntimeException("An error occurred while invoking the Token endpoint: " +
|
||||
errorObject.getDescription() + " (" + errorObject.getCode() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
private LoginAuthenticationToken convertOIDCTokenToLoginAuthenticationToken(OIDCTokenResponse response) throws BadJOSEException, JOSEException, java.text.ParseException, IOException {
|
||||
final OIDCTokens oidcTokens = getOidcTokens(response);
|
||||
final JWT oidcJwt = oidcTokens.getIDToken();
|
||||
|
||||
// Validate the token
|
||||
final IDTokenClaimsSet claimsSet = validateIdToken(oidcJwt);
|
||||
|
||||
// Attempt to extract the configured claim to access the user's identity; default is 'email'
|
||||
String identityClaim = properties.getOidcClaimIdentifyingUser();
|
||||
String identity = claimsSet.getStringClaim(identityClaim);
|
||||
|
||||
// Attempt to extract groups from the configured claim; default is 'groups'
|
||||
String groupsClaim = properties.getOidcClaimGroups();
|
||||
List<String> groups = claimsSet.getStringListClaim(groupsClaim);
|
||||
|
||||
// If default identity not available, attempt secondary identity extraction
|
||||
if (StringUtils.isBlank(identity)) {
|
||||
// Provide clear message to admin that desired claim is missing and present available claims
|
||||
List<String> availableClaims = getAvailableClaims(oidcJwt.getJWTClaimsSet());
|
||||
logger.warn("Failed to obtain the identity of the user with the claim '{}'. The available claims on " +
|
||||
"the OIDC response are: {}. Will attempt to obtain the identity from secondary sources",
|
||||
identityClaim, availableClaims);
|
||||
|
||||
// If the desired user claim was not "email" and "email" is present, use that
|
||||
if (!identityClaim.equalsIgnoreCase(EMAIL_CLAIM) && availableClaims.contains(EMAIL_CLAIM)) {
|
||||
identity = claimsSet.getStringClaim(EMAIL_CLAIM);
|
||||
logger.info("The 'email' claim was present. Using that claim to avoid extra remote call");
|
||||
} else {
|
||||
final List<String> fallbackClaims = properties.getOidcFallbackClaimsIdentifyingUser();
|
||||
for (String fallbackClaim : fallbackClaims) {
|
||||
if (availableClaims.contains(fallbackClaim)) {
|
||||
identity = claimsSet.getStringClaim(fallbackClaim);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (StringUtils.isBlank(identity)) {
|
||||
identity = retrieveIdentityFromUserInfoEndpoint(oidcTokens);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract expiration details from the claims set
|
||||
final Calendar now = Calendar.getInstance();
|
||||
final Date expiration = claimsSet.getExpirationTime();
|
||||
final long expiresIn = expiration.getTime() - now.getTimeInMillis();
|
||||
|
||||
Set<SimpleGrantedAuthority> authorities = groups != null ? groups.stream().map(
|
||||
group -> new SimpleGrantedAuthority(group)).collect(
|
||||
Collectors.collectingAndThen(
|
||||
Collectors.toSet(),
|
||||
Collections::unmodifiableSet
|
||||
)) : null;
|
||||
|
||||
return new LoginAuthenticationToken(
|
||||
identity, identity, expiresIn, claimsSet.getIssuer().getValue(), authorities);
|
||||
}
|
||||
|
||||
private OIDCTokens getOidcTokens(OIDCTokenResponse response) {
|
||||
return response.getOIDCTokens();
|
||||
}
|
||||
|
||||
private String retrieveIdentityFromUserInfoEndpoint(OIDCTokens oidcTokens) throws IOException {
|
||||
// Explicitly try to get the identity from the UserInfo endpoint with the configured claim
|
||||
// Extract the bearer access token
|
||||
final BearerAccessToken bearerAccessToken = oidcTokens.getBearerAccessToken();
|
||||
if (bearerAccessToken == null) {
|
||||
throw new IllegalStateException("No access token found in the ID tokens");
|
||||
}
|
||||
|
||||
// Invoke the UserInfo endpoint
|
||||
HTTPRequest userInfoRequest = createUserInfoRequest(bearerAccessToken);
|
||||
return lookupIdentityInUserInfo(userInfoRequest);
|
||||
}
|
||||
|
||||
private HTTPRequest createTokenHTTPRequest(AuthorizationGrant authorizationGrant, ClientAuthentication clientAuthentication) {
|
||||
final TokenRequest request = new TokenRequest(oidcProviderMetadata.getTokenEndpointURI(), clientAuthentication, authorizationGrant);
|
||||
return formHTTPRequest(request);
|
||||
}
|
||||
|
||||
private HTTPRequest createUserInfoRequest(BearerAccessToken bearerAccessToken) {
|
||||
final UserInfoRequest request = new UserInfoRequest(oidcProviderMetadata.getUserInfoEndpointURI(), bearerAccessToken);
|
||||
return formHTTPRequest(request);
|
||||
}
|
||||
|
||||
private HTTPRequest formHTTPRequest(Request request) {
|
||||
return setHttpRequestProperties(request.toHTTPRequest());
|
||||
}
|
||||
|
||||
private HTTPRequest setHttpRequestProperties(final HTTPRequest httpRequest) {
|
||||
httpRequest.setConnectTimeout(oidcConnectTimeout);
|
||||
httpRequest.setReadTimeout(oidcReadTimeout);
|
||||
if (TruststoreStrategy.NIFI.name().equals(properties.getOidcClientTruststoreStrategy())) {
|
||||
httpRequest.setSSLSocketFactory(sslContext.getSocketFactory());
|
||||
}
|
||||
return httpRequest;
|
||||
}
|
||||
|
||||
private ResourceRetriever getResourceRetriever() {
|
||||
if (TruststoreStrategy.NIFI.name().equals(properties.getOidcClientTruststoreStrategy())) {
|
||||
return new DefaultResourceRetriever(oidcConnectTimeout, oidcReadTimeout, 0, true, sslContext.getSocketFactory());
|
||||
} else {
|
||||
return new DefaultResourceRetriever(oidcConnectTimeout, oidcReadTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
private ClientAuthentication createClientAuthentication() {
|
||||
final ClientAuthentication clientAuthentication;
|
||||
List<ClientAuthenticationMethod> authMethods = oidcProviderMetadata.getTokenEndpointAuthMethods();
|
||||
if (authMethods != null && authMethods.contains(ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
|
||||
clientAuthentication = new ClientSecretPost(clientId, clientSecret);
|
||||
} else {
|
||||
clientAuthentication = new ClientSecretBasic(clientId, clientSecret);
|
||||
}
|
||||
return clientAuthentication;
|
||||
}
|
||||
|
||||
private static List<String> getAvailableClaims(JWTClaimsSet claimSet) {
|
||||
// Get the claims available in the ID token response
|
||||
return claimSet.getClaims().entrySet().stream()
|
||||
// Check claim values are not empty
|
||||
.filter(e -> e.getValue() != null && StringUtils.isNotBlank(e.getValue().toString()))
|
||||
// If not empty, put claim name in a map
|
||||
.map(Map.Entry::getKey)
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void validateAccessToken(OIDCTokens oidcTokens) throws Exception {
|
||||
// Get the Access Token to validate
|
||||
final AccessToken accessToken = oidcTokens.getAccessToken();
|
||||
|
||||
// Get the preferredJwsAlgorithm for validation
|
||||
final JWSAlgorithm jwsAlgorithm = extractJwsAlgorithm();
|
||||
|
||||
// Get the accessTokenHash for validation
|
||||
final String atHashString = oidcTokens
|
||||
.getIDToken()
|
||||
.getJWTClaimsSet()
|
||||
.getStringClaim("at_hash");
|
||||
|
||||
// Compute the Access Token hash
|
||||
final AccessTokenHash atHash = new AccessTokenHash(atHashString);
|
||||
|
||||
try {
|
||||
// Validate the Token
|
||||
AccessTokenValidator.validate(accessToken, jwsAlgorithm, atHash);
|
||||
} catch (InvalidHashException e) {
|
||||
throw new Exception("Unable to validate the Access Token: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private IDTokenClaimsSet validateIdToken(JWT oidcJwt) throws BadJOSEException, JOSEException {
|
||||
try {
|
||||
return tokenValidator.validate(oidcJwt, null);
|
||||
} catch (BadJOSEException e) {
|
||||
throw new BadJOSEException("Unable to validate the ID Token: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String lookupIdentityInUserInfo(final HTTPRequest userInfoHttpRequest) throws IOException {
|
||||
try {
|
||||
// send the user request
|
||||
final UserInfoResponse response = UserInfoResponse.parse(userInfoHttpRequest.send());
|
||||
|
||||
// interpret the details
|
||||
if (response.indicatesSuccess()) {
|
||||
final UserInfoSuccessResponse successResponse = (UserInfoSuccessResponse) response;
|
||||
|
||||
final JWTClaimsSet claimsSet;
|
||||
if (successResponse.getUserInfo() != null) {
|
||||
claimsSet = successResponse.getUserInfo().toJWTClaimsSet();
|
||||
} else {
|
||||
claimsSet = successResponse.getUserInfoJWT().getJWTClaimsSet();
|
||||
}
|
||||
|
||||
final String identity = claimsSet.getStringClaim(properties.getOidcClaimIdentifyingUser());
|
||||
|
||||
// ensure we were able to get the user's identity
|
||||
if (StringUtils.isBlank(identity)) {
|
||||
throw new IllegalStateException("Unable to extract identity from the UserInfo token using the claim '" +
|
||||
properties.getOidcClaimIdentifyingUser() + "'.");
|
||||
} else {
|
||||
return identity;
|
||||
}
|
||||
} else {
|
||||
final UserInfoErrorResponse errorResponse = (UserInfoErrorResponse) response;
|
||||
throw new IdentityAccessException("An error occurred while invoking the UserInfo endpoint: " + errorResponse.getErrorObject().getDescription());
|
||||
}
|
||||
} catch (final ParseException | java.text.ParseException e) {
|
||||
throw new IdentityAccessException("Unable to parse the response from the UserInfo token request: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.client.web;
|
||||
|
||||
import org.apache.nifi.web.security.oidc.revocation.TokenRevocationRequest;
|
||||
import org.apache.nifi.web.security.oidc.revocation.TokenRevocationResponse;
|
||||
import org.apache.nifi.web.security.oidc.revocation.TokenRevocationResponseClient;
|
||||
import org.apache.nifi.web.security.oidc.revocation.TokenTypeHint;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Runnable command to delete expired OpenID Authorized Clients
|
||||
*/
|
||||
public class AuthorizedClientExpirationCommand implements Runnable {
|
||||
private static final Logger logger = LoggerFactory.getLogger(AuthorizedClientExpirationCommand.class);
|
||||
|
||||
private final TrackedAuthorizedClientRepository trackedAuthorizedClientRepository;
|
||||
|
||||
private final TokenRevocationResponseClient tokenRevocationResponseClient;
|
||||
|
||||
public AuthorizedClientExpirationCommand(
|
||||
final TrackedAuthorizedClientRepository trackedAuthorizedClientRepository,
|
||||
final TokenRevocationResponseClient tokenRevocationResponseClient
|
||||
) {
|
||||
this.trackedAuthorizedClientRepository = Objects.requireNonNull(trackedAuthorizedClientRepository, "Repository required");
|
||||
this.tokenRevocationResponseClient = Objects.requireNonNull(tokenRevocationResponseClient, "Response Client required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Run expiration command and send Token Revocation Requests for Refresh Tokens of expired Authorized Clients
|
||||
*/
|
||||
public void run() {
|
||||
logger.debug("Delete Expired Authorized Clients started");
|
||||
final List<OidcAuthorizedClient> deletedAuthorizedClients = deleteExpired();
|
||||
|
||||
for (final OidcAuthorizedClient authorizedClient : deletedAuthorizedClients) {
|
||||
final String identity = authorizedClient.getPrincipalName();
|
||||
final OAuth2RefreshToken refreshToken = authorizedClient.getRefreshToken();
|
||||
if (refreshToken == null) {
|
||||
logger.debug("Identity [{}] OIDC Refresh Token not found", identity);
|
||||
} else {
|
||||
final TokenRevocationRequest revocationRequest = new TokenRevocationRequest(refreshToken.getTokenValue(), TokenTypeHint.REFRESH_TOKEN.getHint());
|
||||
final TokenRevocationResponse revocationResponse = tokenRevocationResponseClient.getRevocationResponse(revocationRequest);
|
||||
logger.debug("Identity [{}] OIDC Refresh Token revocation response status [{}]", identity, revocationResponse.getStatusCode());
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Delete Expired Authorized Clients completed");
|
||||
}
|
||||
|
||||
private List<OidcAuthorizedClient> deleteExpired() {
|
||||
try {
|
||||
return trackedAuthorizedClientRepository.deleteExpired();
|
||||
} catch (final Exception e) {
|
||||
logger.warn("Delete Expired Authorized Clients failed", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.client.web;
|
||||
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* OpenID Connect Authorized Client with ID Token
|
||||
*/
|
||||
public class OidcAuthorizedClient extends OAuth2AuthorizedClient {
|
||||
private final OidcIdToken idToken;
|
||||
|
||||
public OidcAuthorizedClient(
|
||||
final ClientRegistration clientRegistration,
|
||||
final String principalName,
|
||||
final OAuth2AccessToken accessToken,
|
||||
final OAuth2RefreshToken refreshToken,
|
||||
final OidcIdToken idToken
|
||||
) {
|
||||
super(clientRegistration, principalName, accessToken, refreshToken);
|
||||
this.idToken = Objects.requireNonNull(idToken, "OpenID Connect ID Token required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OpenID Connect ID Token
|
||||
*
|
||||
* @return OpenID Connect ID Token
|
||||
*/
|
||||
public OidcIdToken getIdToken() {
|
||||
return idToken;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.client.web;
|
||||
|
||||
import org.apache.nifi.authorization.user.NiFiUserUtils;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieService;
|
||||
import org.apache.nifi.web.security.cookie.StandardApplicationCookieService;
|
||||
import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider;
|
||||
import org.apache.nifi.web.security.jwt.provider.SupportedClaim;
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||
import org.apache.nifi.web.util.RequestUriBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
/**
|
||||
* OpenID Connect Filter for evaluating the application Bearer Token and returning an updated Bearer Token after successful OAuth2 Refresh Token processing
|
||||
*/
|
||||
public class OidcBearerTokenRefreshFilter extends OncePerRequestFilter {
|
||||
private static final String ROOT_PATH = "/";
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OidcBearerTokenRefreshFilter.class);
|
||||
|
||||
private final AntPathRequestMatcher currentUserRequestMatcher = new AntPathRequestMatcher("/flow/current-user");
|
||||
|
||||
private final ApplicationCookieService applicationCookieService = new StandardApplicationCookieService();
|
||||
|
||||
private final Duration refreshWindow;
|
||||
|
||||
private final ConcurrentMap<String, Instant> refreshRequests = new ConcurrentHashMap<>();
|
||||
|
||||
private final BearerTokenProvider bearerTokenProvider;
|
||||
|
||||
private final BearerTokenResolver bearerTokenResolver;
|
||||
|
||||
private final JwtDecoder jwtDecoder;
|
||||
|
||||
private final OAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||
|
||||
private final OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenResponseClient;
|
||||
|
||||
public OidcBearerTokenRefreshFilter(
|
||||
final Duration refreshWindow,
|
||||
final BearerTokenProvider bearerTokenProvider,
|
||||
final BearerTokenResolver bearerTokenResolver,
|
||||
final JwtDecoder jwtDecoder,
|
||||
final OAuth2AuthorizedClientRepository authorizedClientRepository,
|
||||
final OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenResponseClient
|
||||
) {
|
||||
this.refreshWindow = Objects.requireNonNull(refreshWindow, "Refresh Window required");
|
||||
this.bearerTokenProvider = Objects.requireNonNull(bearerTokenProvider, "Bearer Token Provider required");
|
||||
this.bearerTokenResolver = Objects.requireNonNull(bearerTokenResolver, "Bearer Token Resolver required");
|
||||
this.jwtDecoder = Objects.requireNonNull(jwtDecoder, "JWT Decoder required");
|
||||
this.authorizedClientRepository = Objects.requireNonNull(authorizedClientRepository, "Authorized Client Repository required");
|
||||
this.refreshTokenResponseClient = Objects.requireNonNull(refreshTokenResponseClient, "Refresh Token Response Client required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Run Bearer Token Refresh check for matched HTTP Requests and avoid processing multiple requests for the same user
|
||||
*
|
||||
* @param request HTTP Servlet Request
|
||||
* @param response HTTP Servlet Response
|
||||
* @param filterChain Filter Chain
|
||||
* @throws ServletException Thrown on filter processing failures
|
||||
* @throws IOException Thrown on filter processing failures
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
|
||||
if (currentUserRequestMatcher.matches(request)) {
|
||||
final String userIdentity = NiFiUserUtils.getNiFiUserIdentity();
|
||||
if (refreshRequests.putIfAbsent(userIdentity, Instant.now()) == null) {
|
||||
logger.debug("Identity [{}] Bearer Token refresh processing started", userIdentity);
|
||||
try {
|
||||
processRequest(userIdentity, request, response);
|
||||
} catch (final Exception e) {
|
||||
logger.error("Identity [{}] Bearer Token refresh processing failed", userIdentity, e);
|
||||
} finally {
|
||||
refreshRequests.remove(userIdentity);
|
||||
logger.debug("Identity [{}] Bearer Token refresh processing completed", userIdentity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private void processRequest(final String userIdentity, final HttpServletRequest request, final HttpServletResponse response) {
|
||||
if (isRefreshRequired(userIdentity, request)) {
|
||||
logger.info("Identity [{}] Bearer Token refresh required", userIdentity);
|
||||
|
||||
final OidcAuthorizedClient authorizedClient = loadAuthorizedClient(request);
|
||||
if (authorizedClient == null) {
|
||||
logger.warn("Identity [{}] OIDC Authorized Client not found", userIdentity);
|
||||
} else {
|
||||
final OAuth2AccessTokenResponse tokenResponse = getRefreshTokenResponse(authorizedClient, request, response);
|
||||
if (tokenResponse == null) {
|
||||
logger.warn("Identity [{}] OpenID Connect Refresh Token not found", userIdentity);
|
||||
} else {
|
||||
final URI resourceUri = RequestUriBuilder.fromHttpServletRequest(request).path(ROOT_PATH).build();
|
||||
final String issuer = resourceUri.toString();
|
||||
final String bearerToken = getBearerToken(userIdentity, issuer, tokenResponse);
|
||||
applicationCookieService.addSessionCookie(resourceUri, response, ApplicationCookieName.AUTHORIZATION_BEARER, bearerToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRefreshRequired(final String userIdentity, final HttpServletRequest request) {
|
||||
final boolean required;
|
||||
|
||||
final String token = bearerTokenResolver.resolve(request);
|
||||
if (token == null) {
|
||||
logger.debug("Identity [{}] Bearer Token not found", userIdentity);
|
||||
required = false;
|
||||
} else {
|
||||
final Jwt jwt = jwtDecoder.decode(token);
|
||||
final Instant expiresAt = Objects.requireNonNull(jwt.getExpiresAt(), "Bearer Token expiration claim not found");
|
||||
final Instant refreshRequired = Instant.now().plus(refreshWindow);
|
||||
required = refreshRequired.isAfter(expiresAt);
|
||||
}
|
||||
|
||||
return required;
|
||||
}
|
||||
|
||||
private OidcAuthorizedClient loadAuthorizedClient(final HttpServletRequest request) {
|
||||
final SecurityContext context = SecurityContextHolder.getContext();
|
||||
final Authentication principal = context.getAuthentication();
|
||||
return authorizedClientRepository.loadAuthorizedClient(OidcRegistrationProperty.REGISTRATION_ID.getProperty(), principal, request);
|
||||
}
|
||||
|
||||
private String getBearerToken(final String userIdentity, final String issuer, final OAuth2AccessTokenResponse tokenResponse) {
|
||||
final OAuth2AccessToken accessToken = tokenResponse.getAccessToken();
|
||||
final long sessionExpiration = getSessionExpiration(accessToken);
|
||||
final LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(userIdentity, userIdentity, sessionExpiration, issuer);
|
||||
return bearerTokenProvider.getBearerToken(loginAuthenticationToken);
|
||||
}
|
||||
|
||||
private long getSessionExpiration(final OAuth2AccessToken accessToken) {
|
||||
final Instant tokenExpiration = accessToken.getExpiresAt();
|
||||
if (tokenExpiration == null) {
|
||||
throw new IllegalArgumentException("OpenID Connect Access Token expiration claim not found");
|
||||
}
|
||||
final Duration expiration = Duration.between(Instant.now(), tokenExpiration);
|
||||
return expiration.toMillis();
|
||||
}
|
||||
|
||||
private OAuth2AccessTokenResponse getRefreshTokenResponse(final OidcAuthorizedClient authorizedClient, final HttpServletRequest request, final HttpServletResponse response) {
|
||||
final OAuth2AccessTokenResponse tokenResponse;
|
||||
|
||||
if (authorizedClient.getRefreshToken() == null) {
|
||||
tokenResponse = null;
|
||||
} else {
|
||||
tokenResponse = getRefreshTokenResponse(authorizedClient);
|
||||
|
||||
final OAuth2RefreshToken responseRefreshToken = tokenResponse.getRefreshToken();
|
||||
final OAuth2RefreshToken refreshToken = responseRefreshToken == null ? authorizedClient.getRefreshToken() : responseRefreshToken;
|
||||
|
||||
final OidcAuthorizedClient refreshedAuthorizedClient = new OidcAuthorizedClient(
|
||||
authorizedClient.getClientRegistration(),
|
||||
authorizedClient.getPrincipalName(),
|
||||
tokenResponse.getAccessToken(),
|
||||
refreshToken,
|
||||
authorizedClient.getIdToken()
|
||||
);
|
||||
final OAuth2AuthenticationToken authenticationToken = getAuthenticationToken(authorizedClient);
|
||||
authorizedClientRepository.saveAuthorizedClient(refreshedAuthorizedClient, authenticationToken, request, response);
|
||||
}
|
||||
|
||||
return tokenResponse;
|
||||
}
|
||||
|
||||
private OAuth2AccessTokenResponse getRefreshTokenResponse(final OidcAuthorizedClient authorizedClient) {
|
||||
final ClientRegistration clientRegistration = authorizedClient.getClientRegistration();
|
||||
final OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
|
||||
final OAuth2RefreshToken refreshToken = Objects.requireNonNull(authorizedClient.getRefreshToken(), "Refresh Token required");
|
||||
final OAuth2RefreshTokenGrantRequest grantRequest = new OAuth2RefreshTokenGrantRequest(clientRegistration, accessToken, refreshToken);
|
||||
return refreshTokenResponseClient.getTokenResponse(grantRequest);
|
||||
}
|
||||
|
||||
private OAuth2AuthenticationToken getAuthenticationToken(final OidcAuthorizedClient authorizedClient) {
|
||||
final ClientRegistration clientRegistration = authorizedClient.getClientRegistration();
|
||||
final OidcIdToken idToken = authorizedClient.getIdToken();
|
||||
final OidcUser oidcUser = new DefaultOidcUser(Collections.emptyList(), idToken, SupportedClaim.SUBJECT.getClaim());
|
||||
return new OAuth2AuthenticationToken(oidcUser, Collections.emptyList(), clientRegistration.getRegistrationId());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.client.web;
|
||||
|
||||
/**
|
||||
* OpenID Connect configuration property for Registration information
|
||||
*/
|
||||
public enum OidcRegistrationProperty {
|
||||
/** Registration Identifier for URL path matching */
|
||||
REGISTRATION_ID("consumer");
|
||||
|
||||
private final String property;
|
||||
|
||||
OidcRegistrationProperty(final String property) {
|
||||
this.property = property;
|
||||
}
|
||||
|
||||
public String getProperty() {
|
||||
return property;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.client.web;
|
||||
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieService;
|
||||
import org.apache.nifi.web.security.cookie.StandardApplicationCookieService;
|
||||
import org.apache.nifi.web.util.RequestUriBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.net.URI;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* OAuth2 Authorization Request Repository with Spring Cache storage and Request Identifier tracked using HTTP cookies
|
||||
*/
|
||||
public class StandardAuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
|
||||
private static final Logger logger = LoggerFactory.getLogger(StandardAuthorizationRequestRepository.class);
|
||||
|
||||
private static final ApplicationCookieService applicationCookieService = new StandardApplicationCookieService();
|
||||
|
||||
private final Cache cache;
|
||||
|
||||
/**
|
||||
* Standard Authorization Request Repository Constructor
|
||||
*
|
||||
* @param cache Spring Cache for Authorization Requests
|
||||
*/
|
||||
public StandardAuthorizationRequestRepository(final Cache cache) {
|
||||
this.cache = Objects.requireNonNull(cache, "Cache required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Authorization Request using Request Identifier Cookie to lookup cached requests
|
||||
*
|
||||
* @param request HTTP Servlet Request
|
||||
* @return Cached OAuth2 Authorization Request or null when not found
|
||||
*/
|
||||
@Override
|
||||
public OAuth2AuthorizationRequest loadAuthorizationRequest(final HttpServletRequest request) {
|
||||
final Optional<String> requestIdentifier = applicationCookieService.getCookieValue(request, ApplicationCookieName.OIDC_REQUEST_IDENTIFIER);
|
||||
|
||||
final OAuth2AuthorizationRequest authorizationRequest;
|
||||
if (requestIdentifier.isPresent()) {
|
||||
final String identifier = requestIdentifier.get();
|
||||
authorizationRequest = cache.get(identifier, OAuth2AuthorizationRequest.class);
|
||||
if (authorizationRequest == null) {
|
||||
logger.warn("OIDC Authentication Request [{}] not found", identifier);
|
||||
}
|
||||
} else {
|
||||
logger.warn("OIDC Authorization Request Identifier cookie not found");
|
||||
authorizationRequest = null;
|
||||
}
|
||||
|
||||
return authorizationRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save Authorization Request in cache and set HTTP Request Identifier cookie for tracking
|
||||
*
|
||||
* @param authorizationRequest OAuth2 Authorization Request to be cached
|
||||
* @param request HTTP Servlet Request
|
||||
* @param response HTTP Servlet Response
|
||||
*/
|
||||
@Override
|
||||
public void saveAuthorizationRequest(final OAuth2AuthorizationRequest authorizationRequest, final HttpServletRequest request, final HttpServletResponse response) {
|
||||
if (authorizationRequest == null) {
|
||||
removeAuthorizationRequest(request, response);
|
||||
} else {
|
||||
final String identifier = UUID.randomUUID().toString();
|
||||
cache.put(identifier, authorizationRequest);
|
||||
|
||||
final URI resourceUri = RequestUriBuilder.fromHttpServletRequest(request).build();
|
||||
applicationCookieService.addCookie(resourceUri, response, ApplicationCookieName.OIDC_REQUEST_IDENTIFIER, identifier);
|
||||
logger.debug("OIDC Authentication Request [{}] saved", identifier);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove Authorization Request from cache without updating HTTP response cookies
|
||||
*
|
||||
* @param request HTTP Servlet Request
|
||||
* @return OAuth2 Authorization Request removed or null when not found
|
||||
*/
|
||||
@Override
|
||||
public OAuth2AuthorizationRequest removeAuthorizationRequest(final HttpServletRequest request) {
|
||||
return removeAuthorizationRequest(request, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove Authorization Request from cache and remove HTTP cookie with Request Identifier
|
||||
*
|
||||
* @param request HTTP Servlet Request
|
||||
* @param response HTTP Servlet Response
|
||||
* @return OAuth2 Authorization Request removed or null when not found
|
||||
*/
|
||||
@Override
|
||||
public OAuth2AuthorizationRequest removeAuthorizationRequest(final HttpServletRequest request, final HttpServletResponse response) {
|
||||
final OAuth2AuthorizationRequest authorizationRequest = loadAuthorizationRequest(request);
|
||||
if (authorizationRequest == null) {
|
||||
logger.warn("OIDC Authentication Request not removed");
|
||||
} else {
|
||||
if (response == null) {
|
||||
logger.warn("HTTP Servlet Response not specified");
|
||||
} else {
|
||||
final URI resourceUri = RequestUriBuilder.fromHttpServletRequest(request).build();
|
||||
applicationCookieService.removeCookie(resourceUri, response, ApplicationCookieName.OIDC_REQUEST_IDENTIFIER);
|
||||
}
|
||||
|
||||
final Optional<String> requestIdentifier = applicationCookieService.getCookieValue(request, ApplicationCookieName.OIDC_REQUEST_IDENTIFIER);
|
||||
requestIdentifier.ifPresent(cache::evict);
|
||||
}
|
||||
return authorizationRequest;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,276 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.client.web;
|
||||
|
||||
import org.apache.nifi.components.state.Scope;
|
||||
import org.apache.nifi.components.state.StateManager;
|
||||
import org.apache.nifi.components.state.StateMap;
|
||||
import org.apache.nifi.web.security.oidc.client.web.converter.AuthorizedClientConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* OpenID Connect implementation of Authorized Client Repository for storing OAuth2 Tokens
|
||||
*/
|
||||
public class StandardOidcAuthorizedClientRepository implements OAuth2AuthorizedClientRepository, TrackedAuthorizedClientRepository {
|
||||
private static final Logger logger = LoggerFactory.getLogger(StandardOidcAuthorizedClientRepository.class);
|
||||
|
||||
private static final Scope SCOPE = Scope.LOCAL;
|
||||
|
||||
private final StateManager stateManager;
|
||||
|
||||
private final AuthorizedClientConverter authorizedClientConverter;
|
||||
|
||||
/**
|
||||
* Standard OpenID Connect Authorized Client Repository Constructor
|
||||
*
|
||||
* @param stateManager State Manager for storing authorized clients
|
||||
* @param authorizedClientConverter Authorized Client Converter
|
||||
*/
|
||||
public StandardOidcAuthorizedClientRepository(
|
||||
final StateManager stateManager,
|
||||
final AuthorizedClientConverter authorizedClientConverter
|
||||
) {
|
||||
this.stateManager = Objects.requireNonNull(stateManager, "State Manager required");
|
||||
this.authorizedClientConverter = Objects.requireNonNull(authorizedClientConverter, "Authorized Client Converter required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Authorized Client from State Manager
|
||||
*
|
||||
* @param clientRegistrationId the identifier for the client's registration
|
||||
* @param principal Principal Resource Owner to be loaded
|
||||
* @param request HTTP Servlet Request not used
|
||||
* @return Authorized Client or null when not found
|
||||
* @param <T> Authorized Client Type
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(
|
||||
final String clientRegistrationId,
|
||||
final Authentication principal,
|
||||
final HttpServletRequest request
|
||||
) {
|
||||
final OidcAuthorizedClient authorizedClient;
|
||||
|
||||
final String encoded = findEncoded(principal);
|
||||
final String principalId = getPrincipalId(principal);
|
||||
if (encoded == null) {
|
||||
logger.debug("Identity [{}] OIDC Authorized Client not found", principalId);
|
||||
authorizedClient = null;
|
||||
} else {
|
||||
authorizedClient = authorizedClientConverter.getDecoded(encoded);
|
||||
if (authorizedClient == null) {
|
||||
logger.warn("Identity [{}] Removing OIDC Authorized Client after decoding failed", principalId);
|
||||
removeAuthorizedClient(principal);
|
||||
}
|
||||
}
|
||||
|
||||
return (T) authorizedClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save Authorized Client in State Manager
|
||||
*
|
||||
* @param authorizedClient Authorized Client to be saved
|
||||
* @param principal Principal Resource Owner to be saved
|
||||
* @param request HTTP Servlet Request not used
|
||||
* @param response HTTP Servlet Response not used
|
||||
*/
|
||||
@Override
|
||||
public void saveAuthorizedClient(
|
||||
final OAuth2AuthorizedClient authorizedClient,
|
||||
final Authentication principal,
|
||||
final HttpServletRequest request,
|
||||
final HttpServletResponse response
|
||||
) {
|
||||
final OidcAuthorizedClient oidcAuthorizedClient = getOidcAuthorizedClient(authorizedClient, principal);
|
||||
final String encoded = authorizedClientConverter.getEncoded(oidcAuthorizedClient);
|
||||
final String principalId = getPrincipalId(principal);
|
||||
updateState(principalId, stateMap -> stateMap.put(principalId, encoded));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove Authorized Client from State Manager
|
||||
*
|
||||
* @param clientRegistrationId Client Registration Identifier not used
|
||||
* @param principal Principal Resource Owner to be removed
|
||||
* @param request HTTP Servlet Request not used
|
||||
* @param response HTTP Servlet Response not used
|
||||
*/
|
||||
@Override
|
||||
public void removeAuthorizedClient(
|
||||
final String clientRegistrationId,
|
||||
final Authentication principal,
|
||||
final HttpServletRequest request,
|
||||
final HttpServletResponse response
|
||||
) {
|
||||
removeAuthorizedClient(principal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete expired Authorized Clients from the configured State Manager
|
||||
*
|
||||
* @return Deleted OIDC Authorized Clients
|
||||
*/
|
||||
@Override
|
||||
public synchronized List<OidcAuthorizedClient> deleteExpired() {
|
||||
final StateMap stateMap = getStateMap();
|
||||
final Map<String, String> currentStateMap = stateMap.toMap();
|
||||
final Map<String, String> updatedStateMap = new LinkedHashMap<>();
|
||||
|
||||
final List<OidcAuthorizedClient> deletedAuthorizedClients = new ArrayList<>();
|
||||
|
||||
for (final Map.Entry<String, String> encodedEntry : currentStateMap.entrySet()) {
|
||||
final String identity = encodedEntry.getKey();
|
||||
final String encoded = encodedEntry.getValue();
|
||||
try {
|
||||
final OidcAuthorizedClient authorizedClient = authorizedClientConverter.getDecoded(encoded);
|
||||
if (isExpired(authorizedClient)) {
|
||||
deletedAuthorizedClients.add(authorizedClient);
|
||||
} else {
|
||||
updatedStateMap.put(identity, encoded);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
logger.warn("Decoding OIDC Authorized Client [{}] failed", identity, e);
|
||||
}
|
||||
}
|
||||
|
||||
setStateMap(updatedStateMap);
|
||||
if (deletedAuthorizedClients.isEmpty()) {
|
||||
logger.debug("Expired Authorized Clients not found");
|
||||
} else {
|
||||
logger.debug("Deleted Expired Authorized Clients: State before contained [{}] and after [{}]", currentStateMap.size(), updatedStateMap.size());
|
||||
}
|
||||
|
||||
return deletedAuthorizedClients;
|
||||
}
|
||||
|
||||
private boolean isExpired(final OidcAuthorizedClient authorizedClient) {
|
||||
final OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
|
||||
final Instant expiration = accessToken.getExpiresAt();
|
||||
return expiration == null || Instant.now().isAfter(expiration);
|
||||
}
|
||||
|
||||
private void removeAuthorizedClient(final Authentication principal) {
|
||||
final String principalId = getPrincipalId(principal);
|
||||
updateState(principalId, stateMap -> stateMap.remove(principalId));
|
||||
}
|
||||
|
||||
private OidcAuthorizedClient getOidcAuthorizedClient(final OAuth2AuthorizedClient authorizedClient, final Authentication authentication) {
|
||||
final OidcIdToken oidcIdToken = getOidcIdToken(authentication);
|
||||
return new OidcAuthorizedClient(
|
||||
authorizedClient.getClientRegistration(),
|
||||
authorizedClient.getPrincipalName(),
|
||||
authorizedClient.getAccessToken(),
|
||||
authorizedClient.getRefreshToken(),
|
||||
oidcIdToken
|
||||
);
|
||||
}
|
||||
|
||||
private OidcIdToken getOidcIdToken(final Authentication authentication) {
|
||||
final OidcIdToken oidcIdToken;
|
||||
|
||||
if (authentication instanceof OAuth2AuthenticationToken) {
|
||||
final OAuth2AuthenticationToken authenticationToken = (OAuth2AuthenticationToken) authentication;
|
||||
final OAuth2User oAuth2User = authenticationToken.getPrincipal();
|
||||
if (oAuth2User instanceof OidcUser) {
|
||||
final OidcUser oidcUser = (OidcUser) oAuth2User;
|
||||
oidcIdToken = oidcUser.getIdToken();
|
||||
} else {
|
||||
final String message = String.format("OpenID Connect User not found [%s]", oAuth2User.getClass());
|
||||
throw new IllegalArgumentException(message);
|
||||
}
|
||||
} else {
|
||||
final String message = String.format("OpenID Connect Authentication Token not found [%s]", authentication.getClass());
|
||||
throw new IllegalArgumentException(message);
|
||||
}
|
||||
|
||||
return oidcIdToken;
|
||||
}
|
||||
|
||||
private String findEncoded(final Authentication authentication) {
|
||||
final String principalId = getPrincipalId(authentication);
|
||||
final StateMap stateMap = getStateMap();
|
||||
return stateMap.get(principalId);
|
||||
}
|
||||
|
||||
private String getPrincipalId(final Authentication authentication) {
|
||||
return authentication.getName();
|
||||
}
|
||||
|
||||
private synchronized void updateState(final String principalId, final Consumer<Map<String, String>> stateConsumer) {
|
||||
try {
|
||||
final StateMap stateMap = getStateMap();
|
||||
final Map<String, String> currentStateMap = stateMap.toMap();
|
||||
final Map<String, String> updated = new LinkedHashMap<>(currentStateMap);
|
||||
stateConsumer.accept(updated);
|
||||
|
||||
final boolean completed;
|
||||
if (currentStateMap.isEmpty()) {
|
||||
stateManager.setState(updated, SCOPE);
|
||||
completed = true;
|
||||
} else {
|
||||
completed = stateManager.replace(stateMap, updated, SCOPE);
|
||||
}
|
||||
|
||||
if (completed) {
|
||||
logger.info("Identity [{}] OIDC Authorized Client update completed", principalId);
|
||||
} else {
|
||||
logger.info("Identity [{}] OIDC Authorized Client update failed", principalId);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
logger.warn("Identity [{}] OIDC Authorized Client update processing failed", principalId, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void setStateMap(final Map<String, String> stateMap) {
|
||||
try {
|
||||
stateManager.setState(stateMap, SCOPE);
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private StateMap getStateMap() {
|
||||
try {
|
||||
return stateManager.getState(SCOPE);
|
||||
} catch (final IOException e) {
|
||||
throw new UncheckedIOException("Get State for OIDC Authorized Clients failed", e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.client.web;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Tracked abstraction for Authorized Client Repository supporting deletion of expired clients
|
||||
*/
|
||||
public interface TrackedAuthorizedClientRepository {
|
||||
/**
|
||||
* Delete expired Authorized Clients
|
||||
*
|
||||
* @return Deleted OIDC Authorized Clients
|
||||
*/
|
||||
List<OidcAuthorizedClient> deleteExpired();
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.client.web.converter;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
|
||||
|
||||
/**
|
||||
* OAuth2 Authentication Result Converter returns an extended Authentication Token containing the OAuth2 Access Token
|
||||
*/
|
||||
public class AuthenticationResultConverter implements Converter<OAuth2LoginAuthenticationToken, OAuth2AuthenticationToken> {
|
||||
/**
|
||||
* Convert OAuth2 Login Authentication Token to OAuth2 Authentication Token containing the OAuth2 Access Token
|
||||
*
|
||||
* @param source OAuth2 Login Authentication Token
|
||||
* @return Standard extension of OAuth2 Authentication Token
|
||||
*/
|
||||
@Override
|
||||
public OAuth2AuthenticationToken convert(final OAuth2LoginAuthenticationToken source) {
|
||||
return new StandardOAuth2AuthenticationToken(
|
||||
source.getPrincipal(),
|
||||
source.getAuthorities(),
|
||||
source.getClientRegistration().getRegistrationId(),
|
||||
source.getAccessToken()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.client.web.converter;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Serialized representation of Authorized Client
|
||||
*/
|
||||
public class AuthorizedClient {
|
||||
private final String principalName;
|
||||
|
||||
private final AuthorizedToken accessToken;
|
||||
|
||||
private final AuthorizedToken refreshToken;
|
||||
|
||||
private final AuthorizedToken idToken;
|
||||
|
||||
@JsonCreator
|
||||
public AuthorizedClient(
|
||||
@JsonProperty("principalName")
|
||||
final String principalName,
|
||||
@JsonProperty("accessToken")
|
||||
final AuthorizedToken accessToken,
|
||||
@JsonProperty("refreshToken")
|
||||
final AuthorizedToken refreshToken,
|
||||
@JsonProperty("idToken")
|
||||
final AuthorizedToken idToken
|
||||
) {
|
||||
this.principalName = Objects.requireNonNull(principalName, "Principal Name required");
|
||||
this.accessToken = Objects.requireNonNull(accessToken, "Access Token required");
|
||||
this.refreshToken = refreshToken;
|
||||
this.idToken = Objects.requireNonNull(idToken, "ID Token required");
|
||||
}
|
||||
|
||||
public String getPrincipalName() {
|
||||
return principalName;
|
||||
}
|
||||
|
||||
public AuthorizedToken getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public AuthorizedToken getRefreshToken() {
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
public AuthorizedToken getIdToken() {
|
||||
return idToken;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.client.web.converter;
|
||||
|
||||
import org.apache.nifi.web.security.oidc.client.web.OidcAuthorizedClient;
|
||||
|
||||
/**
|
||||
* Abstraction for encoding and decoding OpenID Connect Authorized Client Tokens
|
||||
*/
|
||||
public interface AuthorizedClientConverter {
|
||||
/**
|
||||
* Get encoded representation of Authorized Client
|
||||
*
|
||||
* @param oidcAuthorizedClient OpenID Connect Authorized Client required
|
||||
* @return Encoded representation
|
||||
*/
|
||||
String getEncoded(OidcAuthorizedClient oidcAuthorizedClient);
|
||||
|
||||
/**
|
||||
* Get decoded OpenID Connect Authorized Client
|
||||
*
|
||||
* @param encoded Encoded representation required
|
||||
* @return OpenID Connect Authorized Client
|
||||
*/
|
||||
OidcAuthorizedClient getDecoded(String encoded);
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.client.web.converter;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Serialized representation of Authorized Token with minimum required properties
|
||||
*/
|
||||
public class AuthorizedToken {
|
||||
private final String tokenValue;
|
||||
|
||||
private final Instant issuedAt;
|
||||
|
||||
private final Instant expiresAt;
|
||||
|
||||
@JsonCreator
|
||||
public AuthorizedToken(
|
||||
@JsonProperty("tokenValue")
|
||||
final String tokenValue,
|
||||
@JsonProperty("issuedAt")
|
||||
final Instant issuedAt,
|
||||
@JsonProperty("expiresAt")
|
||||
final Instant expiresAt
|
||||
) {
|
||||
this.tokenValue = Objects.requireNonNull(tokenValue, "Token Value required");
|
||||
this.issuedAt = issuedAt;
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
public String getTokenValue() {
|
||||
return tokenValue;
|
||||
}
|
||||
|
||||
public Instant getIssuedAt() {
|
||||
return issuedAt;
|
||||
}
|
||||
|
||||
public Instant getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.client.web.converter;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.apache.nifi.encrypt.PropertyEncryptor;
|
||||
import org.apache.nifi.web.security.jwt.provider.SupportedClaim;
|
||||
import org.apache.nifi.web.security.oidc.OidcConfigurationException;
|
||||
import org.apache.nifi.web.security.oidc.client.web.OidcAuthorizedClient;
|
||||
import org.apache.nifi.web.security.oidc.client.web.OidcRegistrationProperty;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Standard Authorized Client Converter with JSON serialization and encryption
|
||||
*/
|
||||
public class StandardAuthorizedClientConverter implements AuthorizedClientConverter {
|
||||
private static final Logger logger = LoggerFactory.getLogger(StandardAuthorizedClientConverter.class);
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModules(new JavaTimeModule());
|
||||
|
||||
private final PropertyEncryptor propertyEncryptor;
|
||||
|
||||
private final ClientRegistrationRepository clientRegistrationRepository;
|
||||
|
||||
public StandardAuthorizedClientConverter(
|
||||
final PropertyEncryptor propertyEncryptor,
|
||||
final ClientRegistrationRepository clientRegistrationRepository
|
||||
) {
|
||||
this.propertyEncryptor = Objects.requireNonNull(propertyEncryptor, "Property Encryptor required");
|
||||
this.clientRegistrationRepository = Objects.requireNonNull(clientRegistrationRepository, "Client Registry Repository required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get encoded representation serializes as JSON and returns an encrypted string
|
||||
*
|
||||
* @param oidcAuthorizedClient OpenID Connect Authorized Client required
|
||||
* @return JSON serialized encrypted string
|
||||
*/
|
||||
@Override
|
||||
public String getEncoded(final OidcAuthorizedClient oidcAuthorizedClient) {
|
||||
Objects.requireNonNull(oidcAuthorizedClient, "Authorized Client required");
|
||||
|
||||
try {
|
||||
final AuthorizedClient authorizedClient = writeAuthorizedClient(oidcAuthorizedClient);
|
||||
final String serialized = OBJECT_MAPPER.writeValueAsString(authorizedClient);
|
||||
return propertyEncryptor.encrypt(serialized);
|
||||
} catch (final Exception e) {
|
||||
throw new OidcConfigurationException("OIDC Authorized Client serialization failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get decoded Authorized Client from encrypted string containing JSON
|
||||
*
|
||||
* @param encoded Encoded representation required
|
||||
* @return OpenID Connect Authorized Client or null on decoding failures
|
||||
*/
|
||||
@Override
|
||||
public OidcAuthorizedClient getDecoded(final String encoded) {
|
||||
Objects.requireNonNull(encoded, "Encoded representation required");
|
||||
|
||||
try {
|
||||
final String decrypted = propertyEncryptor.decrypt(encoded);
|
||||
final AuthorizedClient authorizedClient = OBJECT_MAPPER.readValue(decrypted, AuthorizedClient.class);
|
||||
return readAuthorizedClient(authorizedClient);
|
||||
} catch (final Exception e) {
|
||||
logger.warn("OIDC Authorized Client decoding failed", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private AuthorizedClient writeAuthorizedClient(final OidcAuthorizedClient oidcAuthorizedClient) {
|
||||
final OAuth2AccessToken oidcAccessToken = oidcAuthorizedClient.getAccessToken();
|
||||
final AuthorizedToken accessToken = new AuthorizedToken(
|
||||
oidcAccessToken.getTokenValue(),
|
||||
oidcAccessToken.getIssuedAt(),
|
||||
oidcAccessToken.getExpiresAt()
|
||||
);
|
||||
|
||||
final AuthorizedToken refreshToken;
|
||||
final OAuth2RefreshToken oidcRefreshToken = oidcAuthorizedClient.getRefreshToken();
|
||||
if (oidcRefreshToken == null) {
|
||||
refreshToken = null;
|
||||
} else {
|
||||
refreshToken = new AuthorizedToken(
|
||||
oidcRefreshToken.getTokenValue(),
|
||||
oidcRefreshToken.getIssuedAt(),
|
||||
oidcRefreshToken.getExpiresAt()
|
||||
);
|
||||
}
|
||||
|
||||
final OidcIdToken oidcIdToken = oidcAuthorizedClient.getIdToken();
|
||||
final AuthorizedToken idToken = new AuthorizedToken(
|
||||
oidcIdToken.getTokenValue(),
|
||||
oidcIdToken.getIssuedAt(),
|
||||
oidcIdToken.getExpiresAt()
|
||||
);
|
||||
|
||||
final String principalName = oidcAuthorizedClient.getPrincipalName();
|
||||
return new AuthorizedClient(principalName, accessToken, refreshToken, idToken);
|
||||
}
|
||||
|
||||
private OidcAuthorizedClient readAuthorizedClient(final AuthorizedClient authorizedClient) {
|
||||
final ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(OidcRegistrationProperty.REGISTRATION_ID.getProperty());
|
||||
|
||||
final String principalName = authorizedClient.getPrincipalName();
|
||||
final OAuth2AccessToken accessToken = getAccessToken(authorizedClient.getAccessToken());
|
||||
final OAuth2RefreshToken refreshToken = getRefreshToken(authorizedClient.getRefreshToken());
|
||||
final OidcIdToken idToken = getIdToken(authorizedClient);
|
||||
|
||||
return new OidcAuthorizedClient(clientRegistration, principalName, accessToken, refreshToken, idToken);
|
||||
}
|
||||
|
||||
private OAuth2AccessToken getAccessToken(final AuthorizedToken authorizedToken) {
|
||||
return new OAuth2AccessToken(
|
||||
OAuth2AccessToken.TokenType.BEARER,
|
||||
authorizedToken.getTokenValue(),
|
||||
authorizedToken.getIssuedAt(),
|
||||
authorizedToken.getExpiresAt()
|
||||
);
|
||||
}
|
||||
|
||||
private OAuth2RefreshToken getRefreshToken(final AuthorizedToken authorizedToken) {
|
||||
return authorizedToken == null ? null : new OAuth2RefreshToken(
|
||||
authorizedToken.getTokenValue(),
|
||||
authorizedToken.getIssuedAt(),
|
||||
authorizedToken.getExpiresAt()
|
||||
);
|
||||
}
|
||||
|
||||
private OidcIdToken getIdToken(final AuthorizedClient authorizedClient) {
|
||||
final AuthorizedToken authorizedToken = authorizedClient.getIdToken();
|
||||
|
||||
final Map<String, Object> claims = new LinkedHashMap<>();
|
||||
claims.put(SupportedClaim.SUBJECT.getClaim(), authorizedClient.getPrincipalName());
|
||||
claims.put(SupportedClaim.ISSUED_AT.getClaim(), authorizedToken.getIssuedAt());
|
||||
claims.put(SupportedClaim.EXPIRATION.getClaim(), authorizedToken.getExpiresAt());
|
||||
return new OidcIdToken(
|
||||
authorizedToken.getTokenValue(),
|
||||
authorizedToken.getIssuedAt(),
|
||||
authorizedToken.getExpiresAt(),
|
||||
claims
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.client.web.converter;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* OAuth2 Authentication Token extended to include the OAuth2 Access Token
|
||||
*/
|
||||
public class StandardOAuth2AuthenticationToken extends OAuth2AuthenticationToken {
|
||||
private final OAuth2AccessToken accessToken;
|
||||
|
||||
public StandardOAuth2AuthenticationToken(
|
||||
final OAuth2User principal,
|
||||
final Collection<? extends GrantedAuthority> authorities,
|
||||
final String authorizedClientRegistrationId,
|
||||
final OAuth2AccessToken accessToken
|
||||
) {
|
||||
super(principal, authorities, authorizedClientRegistrationId);
|
||||
this.accessToken = Objects.requireNonNull(accessToken, "Access Token required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Credentials returns the OAuth2 Access Token
|
||||
*
|
||||
* @return OAuth2 Access Token
|
||||
*/
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return accessToken;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.logout;
|
||||
|
||||
import org.apache.nifi.web.security.logout.StandardLogoutFilter;
|
||||
import org.apache.nifi.web.security.oidc.OidcUrlPath;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
|
||||
/**
|
||||
* OpenID Connect Logout Filter completes application Logout Requests
|
||||
*/
|
||||
public class OidcLogoutFilter extends StandardLogoutFilter {
|
||||
public OidcLogoutFilter(
|
||||
final LogoutSuccessHandler logoutSuccessHandler
|
||||
) {
|
||||
super(new AntPathRequestMatcher(OidcUrlPath.LOGOUT.getPath()), logoutSuccessHandler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,225 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.logout;
|
||||
|
||||
import org.apache.nifi.admin.service.IdpUserGroupService;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieService;
|
||||
import org.apache.nifi.web.security.cookie.StandardApplicationCookieService;
|
||||
import org.apache.nifi.web.security.logout.LogoutRequest;
|
||||
import org.apache.nifi.web.security.logout.LogoutRequestManager;
|
||||
import org.apache.nifi.web.security.oidc.client.web.OidcRegistrationProperty;
|
||||
import org.apache.nifi.web.security.oidc.client.web.OidcAuthorizedClient;
|
||||
import org.apache.nifi.web.security.oidc.revocation.TokenRevocationRequest;
|
||||
import org.apache.nifi.web.security.oidc.revocation.TokenRevocationResponse;
|
||||
import org.apache.nifi.web.security.oidc.revocation.TokenRevocationResponseClient;
|
||||
import org.apache.nifi.web.security.oidc.revocation.TokenTypeHint;
|
||||
import org.apache.nifi.web.security.token.LogoutAuthenticationToken;
|
||||
import org.apache.nifi.web.util.RequestUriBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* OpenID Connect Logout Success Handler supports RP-Initiated Logout and
|
||||
* completes Logout Requests with conditional redirect to Authorization Server
|
||||
*/
|
||||
public class OidcLogoutSuccessHandler implements LogoutSuccessHandler {
|
||||
static final String END_SESSION_ENDPOINT = "end_session_endpoint";
|
||||
|
||||
private static final String LOGOUT_COMPLETE_PATH = "/nifi/logout-complete";
|
||||
|
||||
private static final String ID_TOKEN_HINT_PARAMETER = "id_token_hint";
|
||||
|
||||
private static final String POST_LOGOUT_REDIRECT_URI_PARAMETER = "post_logout_redirect_uri";
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OidcLogoutSuccessHandler.class);
|
||||
|
||||
private final ApplicationCookieService applicationCookieService = new StandardApplicationCookieService();
|
||||
|
||||
private final LogoutRequestManager logoutRequestManager;
|
||||
|
||||
private final IdpUserGroupService idpUserGroupService;
|
||||
|
||||
private final ClientRegistrationRepository clientRegistrationRepository;
|
||||
|
||||
private final OAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||
|
||||
private final TokenRevocationResponseClient tokenRevocationResponseClient;
|
||||
|
||||
/**
|
||||
* OpenID Connect Logout Success Handler with RP-Initiated Logout 1.0 and RFC 7009 Token Revocation
|
||||
*
|
||||
* @param logoutRequestManager Application Logout Request Manager
|
||||
* @param idpUserGroupService User Group Service for clearing cached groups
|
||||
* @param clientRegistrationRepository OIDC Client Registry Repository for configuration information
|
||||
* @param authorizedClientRepository OIDC Authorized Client Repository for cached tokens
|
||||
* @param tokenRevocationResponseClient OIDC Revocation Response Client for revoking Refresh Tokens
|
||||
*/
|
||||
public OidcLogoutSuccessHandler(
|
||||
final LogoutRequestManager logoutRequestManager,
|
||||
final IdpUserGroupService idpUserGroupService,
|
||||
final ClientRegistrationRepository clientRegistrationRepository,
|
||||
final OAuth2AuthorizedClientRepository authorizedClientRepository,
|
||||
final TokenRevocationResponseClient tokenRevocationResponseClient
|
||||
) {
|
||||
this.logoutRequestManager = Objects.requireNonNull(logoutRequestManager, "Logout Request Manager required");
|
||||
this.idpUserGroupService = Objects.requireNonNull(idpUserGroupService, "User Group Service required");
|
||||
this.clientRegistrationRepository = Objects.requireNonNull(clientRegistrationRepository, "Client Registration Repository required");
|
||||
this.authorizedClientRepository = Objects.requireNonNull(authorizedClientRepository, "Authorized Client Repository required");
|
||||
this.tokenRevocationResponseClient = Objects.requireNonNull(tokenRevocationResponseClient, "Revocation Response Client required");
|
||||
}
|
||||
|
||||
/**
|
||||
* On Logout Success complete Logout Request based on Logout Request Identifier found in cookies
|
||||
*
|
||||
* @param request HTTP Servlet Request
|
||||
* @param response HTTP Servlet Response
|
||||
* @param authentication Authentication is not used
|
||||
* @throws IOException Thrown on HttpServletResponse.sendRedirect() failures
|
||||
*/
|
||||
@Override
|
||||
public void onLogoutSuccess(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) throws IOException {
|
||||
final Optional<String> logoutRequestIdentifier = applicationCookieService.getCookieValue(request, ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER);
|
||||
if (logoutRequestIdentifier.isPresent()) {
|
||||
final String targetUrl;
|
||||
|
||||
final String requestIdentifier = logoutRequestIdentifier.get();
|
||||
final LogoutRequest logoutRequest = logoutRequestManager.get(requestIdentifier);
|
||||
if (logoutRequest == null) {
|
||||
logger.warn("OIDC Logout Request [{}] not found", requestIdentifier);
|
||||
targetUrl = getPostLogoutRedirectUri(request);
|
||||
} else {
|
||||
final String mappedUserIdentity = logoutRequest.getMappedUserIdentity();
|
||||
idpUserGroupService.deleteUserGroups(mappedUserIdentity);
|
||||
targetUrl = processLogoutRequest(request, response, requestIdentifier, mappedUserIdentity);
|
||||
}
|
||||
|
||||
response.sendRedirect(targetUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private String processLogoutRequest(final HttpServletRequest request, final HttpServletResponse response, final String requestIdentifier, final String mappedUserIdentity) {
|
||||
final String targetUrl;
|
||||
|
||||
final ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(OidcRegistrationProperty.REGISTRATION_ID.getProperty());
|
||||
|
||||
final OidcAuthorizedClient oidcAuthorizedClient = getOidcAuthorizedClient(clientRegistration, mappedUserIdentity, request);
|
||||
if (oidcAuthorizedClient == null) {
|
||||
targetUrl = getPostLogoutRedirectUri(request);
|
||||
logger.warn("OIDC Logout Request [{}] Identity [{}] ID Token not found", requestIdentifier, mappedUserIdentity);
|
||||
} else {
|
||||
final URI endSessionEndpoint = getEndSessionEndpoint(clientRegistration);
|
||||
if (endSessionEndpoint == null) {
|
||||
targetUrl = getPostLogoutRedirectUri(request);
|
||||
logger.debug("OIDC Logout Request [{}] Identity [{}] end_session_endpoint not provided", requestIdentifier, mappedUserIdentity);
|
||||
} else {
|
||||
final String postLogoutRedirectUri = getPostLogoutRedirectUri(request);
|
||||
|
||||
final OidcIdToken oidcIdToken = oidcAuthorizedClient.getIdToken();
|
||||
final String idToken = oidcIdToken.getTokenValue();
|
||||
targetUrl = getEndSessionTargetUrl(endSessionEndpoint, idToken, postLogoutRedirectUri);
|
||||
logger.info("OIDC Logout Request [{}] Identity [{}] initiated", requestIdentifier, mappedUserIdentity);
|
||||
}
|
||||
|
||||
final LogoutAuthenticationToken principal = new LogoutAuthenticationToken(mappedUserIdentity);
|
||||
authorizedClientRepository.removeAuthorizedClient(OidcRegistrationProperty.REGISTRATION_ID.getProperty(), principal, request, response);
|
||||
processRefreshTokenRevocation(oidcAuthorizedClient, mappedUserIdentity);
|
||||
processAccessTokenRevocation(oidcAuthorizedClient, mappedUserIdentity);
|
||||
}
|
||||
|
||||
return targetUrl;
|
||||
}
|
||||
|
||||
private void processAccessTokenRevocation(final OidcAuthorizedClient oidcAuthorizedClient, final String userIdentity) {
|
||||
final OAuth2AccessToken accessToken = oidcAuthorizedClient.getAccessToken();
|
||||
final String token = accessToken.getTokenValue();
|
||||
final TokenRevocationRequest revocationRequest = new TokenRevocationRequest(token, TokenTypeHint.ACCESS_TOKEN.getHint());
|
||||
final TokenRevocationResponse revocationResponse = tokenRevocationResponseClient.getRevocationResponse(revocationRequest);
|
||||
logger.info("Identity [{}] OIDC Access Token Revocation completed [HTTP {}]", userIdentity, revocationResponse.getStatusCode());
|
||||
}
|
||||
|
||||
private void processRefreshTokenRevocation(final OidcAuthorizedClient oidcAuthorizedClient, final String userIdentity) {
|
||||
final OAuth2RefreshToken refreshToken = oidcAuthorizedClient.getRefreshToken();
|
||||
if (refreshToken == null) {
|
||||
logger.debug("Identity [{}] OIDC Refresh Token not found for revocation", userIdentity);
|
||||
} else {
|
||||
final String token = refreshToken.getTokenValue();
|
||||
final TokenRevocationRequest revocationRequest = new TokenRevocationRequest(token, TokenTypeHint.REFRESH_TOKEN.getHint());
|
||||
final TokenRevocationResponse revocationResponse = tokenRevocationResponseClient.getRevocationResponse(revocationRequest);
|
||||
logger.info("Identity [{}] OIDC Refresh Token Revocation completed [HTTP {}]", userIdentity, revocationResponse.getStatusCode());
|
||||
}
|
||||
}
|
||||
|
||||
private OidcAuthorizedClient getOidcAuthorizedClient(final ClientRegistration clientRegistration, final String userIdentity, final HttpServletRequest request) {
|
||||
final OidcAuthorizedClient oidcAuthorizedClient;
|
||||
|
||||
final String clientRegistrationId = clientRegistration.getRegistrationId();
|
||||
final LogoutAuthenticationToken authenticationToken = new LogoutAuthenticationToken(userIdentity);
|
||||
final OAuth2AuthorizedClient authorizedClient = authorizedClientRepository.loadAuthorizedClient(clientRegistrationId, authenticationToken, request);
|
||||
if (authorizedClient == null) {
|
||||
logger.warn("Identity [{}] OpenID Connect Authorized Client not found", userIdentity);
|
||||
oidcAuthorizedClient = null;
|
||||
} else if (authorizedClient instanceof OidcAuthorizedClient) {
|
||||
oidcAuthorizedClient = (OidcAuthorizedClient) authorizedClient;
|
||||
} else {
|
||||
logger.error("Identity [{}] OpenID Connect Authorized Client Class not found [{}]", userIdentity, authorizedClient.getClass());
|
||||
oidcAuthorizedClient = null;
|
||||
}
|
||||
|
||||
return oidcAuthorizedClient;
|
||||
}
|
||||
|
||||
private URI getEndSessionEndpoint(final ClientRegistration clientRegistration) {
|
||||
final ClientRegistration.ProviderDetails providerDetails = clientRegistration.getProviderDetails();
|
||||
final Map<String, Object> configurationMetadata = providerDetails.getConfigurationMetadata();
|
||||
final Object endSessionEndpoint = configurationMetadata.get(END_SESSION_ENDPOINT);
|
||||
return endSessionEndpoint == null ? null : URI.create(endSessionEndpoint.toString());
|
||||
}
|
||||
|
||||
private String getEndSessionTargetUrl(final URI endSessionEndpoint, final String idToken, final String postLogoutRedirectUri) {
|
||||
final UriComponentsBuilder builder = UriComponentsBuilder.fromUri(endSessionEndpoint);
|
||||
builder.queryParam(ID_TOKEN_HINT_PARAMETER, idToken);
|
||||
builder.queryParam(POST_LOGOUT_REDIRECT_URI_PARAMETER, postLogoutRedirectUri);
|
||||
return builder.encode(StandardCharsets.UTF_8).build().toUriString();
|
||||
}
|
||||
|
||||
private String getPostLogoutRedirectUri(final HttpServletRequest request) {
|
||||
return RequestUriBuilder.fromHttpServletRequest(request)
|
||||
.path(LOGOUT_COMPLETE_PATH)
|
||||
.build()
|
||||
.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.registration;
|
||||
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
|
||||
/**
|
||||
* Provider interface for OpenID Connect Client Registration abstracting access to metadata from a configurable location
|
||||
*/
|
||||
public interface ClientRegistrationProvider {
|
||||
/**
|
||||
* Get Client Registration
|
||||
*
|
||||
* @return OAuth2 Client Registration
|
||||
*/
|
||||
ClientRegistration getClientRegistration();
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.registration;
|
||||
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
|
||||
/**
|
||||
* Disabled implementation of Client Registration Repository when OpenID Connect is not configured
|
||||
*/
|
||||
public class DisabledClientRegistrationRepository implements ClientRegistrationRepository {
|
||||
|
||||
@Override
|
||||
public ClientRegistration findByRegistrationId(final String registrationId) {
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.registration;
|
||||
|
||||
import static com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
|
||||
import static com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_POST;
|
||||
import static com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.NONE;
|
||||
|
||||
import com.nimbusds.oauth2.sdk.ParseException;
|
||||
import com.nimbusds.oauth2.sdk.Scope;
|
||||
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.oidc.OidcConfigurationException;
|
||||
import org.apache.nifi.web.security.oidc.OidcUrlPath;
|
||||
import org.apache.nifi.web.security.oidc.client.web.OidcRegistrationProperty;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.web.client.RestOperations;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Standard implementation of Client Registration Provider using Application Properties
|
||||
*/
|
||||
public class StandardClientRegistrationProvider implements ClientRegistrationProvider {
|
||||
|
||||
private static final String REGISTRATION_REDIRECT_URI = String.format("{baseUrl}%s", OidcUrlPath.CALLBACK.getPath());
|
||||
|
||||
private final NiFiProperties properties;
|
||||
|
||||
private final RestOperations restOperations;
|
||||
|
||||
public StandardClientRegistrationProvider(
|
||||
final NiFiProperties properties,
|
||||
final RestOperations restOperations
|
||||
) {
|
||||
this.properties = Objects.requireNonNull(properties, "Properties required");
|
||||
this.restOperations = Objects.requireNonNull(restOperations, "REST Operations required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Client Registration using OpenID Connect Discovery URL
|
||||
*
|
||||
* @return Client Registration
|
||||
*/
|
||||
@Override
|
||||
public ClientRegistration getClientRegistration() {
|
||||
final String clientId = properties.getOidcClientId();
|
||||
final String clientSecret = properties.getOidcClientSecret();
|
||||
|
||||
final OIDCProviderMetadata providerMetadata = getProviderMetadata();
|
||||
final ClientAuthenticationMethod clientAuthenticationMethod = getClientAuthenticationMethod(providerMetadata.getTokenEndpointAuthMethods());
|
||||
final String issuerUri = providerMetadata.getIssuer().getValue();
|
||||
final String tokenUri = providerMetadata.getTokenEndpointURI().toASCIIString();
|
||||
final Map<String, Object> configurationMetadata = new LinkedHashMap<>(providerMetadata.toJSONObject());
|
||||
final String authorizationUri = providerMetadata.getAuthorizationEndpointURI().toASCIIString();
|
||||
final String jwkSetUri = providerMetadata.getJWKSetURI().toASCIIString();
|
||||
final String userInfoUri = providerMetadata.getUserInfoEndpointURI().toASCIIString();
|
||||
|
||||
final Scope metadataScope = providerMetadata.getScopes();
|
||||
final Set<String> scope = new LinkedHashSet<>(metadataScope.toStringList());
|
||||
final List<String> additionalScopes = properties.getOidcAdditionalScopes();
|
||||
scope.addAll(additionalScopes);
|
||||
|
||||
final String userNameAttributeName = properties.getOidcClaimIdentifyingUser();
|
||||
|
||||
return ClientRegistration.withRegistrationId(OidcRegistrationProperty.REGISTRATION_ID.getProperty())
|
||||
.clientId(clientId)
|
||||
.clientSecret(clientSecret)
|
||||
.clientName(issuerUri)
|
||||
.issuerUri(issuerUri)
|
||||
.tokenUri(tokenUri)
|
||||
.authorizationUri(authorizationUri)
|
||||
.jwkSetUri(jwkSetUri)
|
||||
.userInfoUri(userInfoUri)
|
||||
.providerConfigurationMetadata(configurationMetadata)
|
||||
.redirectUri(REGISTRATION_REDIRECT_URI)
|
||||
.scope(scope)
|
||||
.userNameAttributeName(userNameAttributeName)
|
||||
.clientAuthenticationMethod(clientAuthenticationMethod)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.build();
|
||||
}
|
||||
|
||||
private OIDCProviderMetadata getProviderMetadata() {
|
||||
final String discoveryUrl = properties.getOidcDiscoveryUrl();
|
||||
|
||||
final String metadataObject;
|
||||
try {
|
||||
metadataObject = restOperations.getForObject(discoveryUrl, String.class);
|
||||
} catch (final RuntimeException e) {
|
||||
final String message = String.format("OpenID Connect Metadata URL [%s] retrieval failed", discoveryUrl);
|
||||
throw new OidcConfigurationException(message, e);
|
||||
}
|
||||
|
||||
try {
|
||||
return OIDCProviderMetadata.parse(metadataObject);
|
||||
} catch (final ParseException e) {
|
||||
throw new OidcConfigurationException("OpenID Connect Metadata parsing failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private ClientAuthenticationMethod getClientAuthenticationMethod(final List<com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod> metadataAuthMethods) {
|
||||
final ClientAuthenticationMethod clientAuthenticationMethod;
|
||||
|
||||
if (metadataAuthMethods == null || metadataAuthMethods.contains(CLIENT_SECRET_BASIC)) {
|
||||
clientAuthenticationMethod = ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
|
||||
} else if (metadataAuthMethods.contains(CLIENT_SECRET_POST)) {
|
||||
clientAuthenticationMethod = ClientAuthenticationMethod.CLIENT_SECRET_POST;
|
||||
} else if (metadataAuthMethods.contains(NONE)) {
|
||||
clientAuthenticationMethod = ClientAuthenticationMethod.NONE;
|
||||
} else {
|
||||
clientAuthenticationMethod = ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
|
||||
}
|
||||
|
||||
return clientAuthenticationMethod;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.revocation;
|
||||
|
||||
import org.apache.nifi.web.security.oidc.client.web.OidcRegistrationProperty;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestOperations;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Standard implementation for handling Token Revocation Requests using Spring REST Operations
|
||||
*/
|
||||
public class StandardTokenRevocationResponseClient implements TokenRevocationResponseClient {
|
||||
static final String REVOCATION_ENDPOINT = "revocation_endpoint";
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(StandardTokenRevocationResponseClient.class);
|
||||
|
||||
private final RestOperations restOperations;
|
||||
|
||||
private final ClientRegistrationRepository clientRegistrationRepository;
|
||||
|
||||
public StandardTokenRevocationResponseClient(
|
||||
final RestOperations restOperations,
|
||||
final ClientRegistrationRepository clientRegistrationRepository
|
||||
) {
|
||||
this.restOperations = Objects.requireNonNull(restOperations, "REST Operations required");
|
||||
this.clientRegistrationRepository = Objects.requireNonNull(clientRegistrationRepository, "Client Registry Repository required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Revocation Response as described in RFC 7009 Section 2.2 or return success when the Revocation Endpoint is not configured
|
||||
*
|
||||
* @param revocationRequest Revocation Request is required
|
||||
* @return Token Revocation Response
|
||||
*/
|
||||
@Override
|
||||
public TokenRevocationResponse getRevocationResponse(final TokenRevocationRequest revocationRequest) {
|
||||
Objects.requireNonNull(revocationRequest, "Revocation Request required");
|
||||
|
||||
final ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(OidcRegistrationProperty.REGISTRATION_ID.getProperty());
|
||||
final ResponseEntity<?> responseEntity = getResponseEntity(revocationRequest, clientRegistration);
|
||||
final HttpStatus statusCode = responseEntity.getStatusCode();
|
||||
return new TokenRevocationResponse(statusCode.is2xxSuccessful(), statusCode.value());
|
||||
}
|
||||
|
||||
private ResponseEntity<?> getResponseEntity(final TokenRevocationRequest revocationRequest, final ClientRegistration clientRegistration) {
|
||||
final RequestEntity<?> requestEntity = getRequestEntity(revocationRequest, clientRegistration);
|
||||
if (requestEntity == null) {
|
||||
return ResponseEntity.ok(null);
|
||||
} else {
|
||||
try {
|
||||
final ResponseEntity<?> responseEntity = restOperations.exchange(requestEntity, String.class);
|
||||
logger.debug("Token Revocation Request processing completed [HTTP {}]", responseEntity.getStatusCode());
|
||||
return responseEntity;
|
||||
} catch (final Exception e) {
|
||||
logger.warn("Token Revocation Request processing failed", e);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private RequestEntity<?> getRequestEntity(final TokenRevocationRequest revocationRequest, final ClientRegistration clientRegistration) {
|
||||
final RequestEntity<?> requestEntity;
|
||||
final URI revocationEndpoint = getRevocationEndpoint(clientRegistration);
|
||||
if (revocationEndpoint == null) {
|
||||
requestEntity = null;
|
||||
logger.info("OIDC Revocation Endpoint not found");
|
||||
} else {
|
||||
final LinkedMultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.add(OAuth2ParameterNames.TOKEN, revocationRequest.getToken());
|
||||
|
||||
final String tokenTypeHint = revocationRequest.getTokenTypeHint();
|
||||
if (StringUtils.hasLength(tokenTypeHint)) {
|
||||
parameters.add(OAuth2ParameterNames.TOKEN_TYPE_HINT, tokenTypeHint);
|
||||
}
|
||||
|
||||
final HttpHeaders headers = new HttpHeaders();
|
||||
final String clientId = clientRegistration.getClientId();
|
||||
final String clientSecret = clientRegistration.getClientSecret();
|
||||
headers.setBasicAuth(clientId, clientSecret, StandardCharsets.UTF_8);
|
||||
|
||||
requestEntity = RequestEntity.post(revocationEndpoint)
|
||||
.headers(headers)
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.body(parameters);
|
||||
}
|
||||
return requestEntity;
|
||||
}
|
||||
|
||||
private URI getRevocationEndpoint(final ClientRegistration clientRegistration) {
|
||||
final ClientRegistration.ProviderDetails providerDetails = clientRegistration.getProviderDetails();
|
||||
final Map<String, Object> configurationMetadata = providerDetails.getConfigurationMetadata();
|
||||
final Object revocationEndpoint = configurationMetadata.get(REVOCATION_ENDPOINT);
|
||||
return revocationEndpoint == null ? null : URI.create(revocationEndpoint.toString());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.revocation;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* OAuth2 Token Revocation Request as described in RFC 7009 Section 2.1
|
||||
*/
|
||||
public class TokenRevocationRequest {
|
||||
private final String token;
|
||||
|
||||
private final String tokenTypeHint;
|
||||
|
||||
public TokenRevocationRequest(
|
||||
final String token,
|
||||
final String tokenTypeHint
|
||||
) {
|
||||
this.token = Objects.requireNonNull(token, "Token required");
|
||||
this.tokenTypeHint = tokenTypeHint;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public String getTokenTypeHint() {
|
||||
return tokenTypeHint;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.revocation;
|
||||
|
||||
/**
|
||||
* OAuth2 Token Revocation Request as described in RFC 7009 Section 2.1
|
||||
*/
|
||||
public class TokenRevocationResponse {
|
||||
private final boolean success;
|
||||
|
||||
private final int statusCode;
|
||||
|
||||
public TokenRevocationResponse(final boolean success, final int statusCode) {
|
||||
this.success = success;
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public int getStatusCode() {
|
||||
return statusCode;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.revocation;
|
||||
|
||||
/**
|
||||
* Abstraction for processing Token Revocation requests according to RFC 7009
|
||||
*/
|
||||
public interface TokenRevocationResponseClient {
|
||||
/**
|
||||
* Get Token Revocation Response based on Revocation Request
|
||||
*
|
||||
* @param revocationRequest Revocation Request is required
|
||||
* @return Token Revocation Response
|
||||
*/
|
||||
TokenRevocationResponse getRevocationResponse(TokenRevocationRequest revocationRequest);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.revocation;
|
||||
|
||||
/**
|
||||
* Token Type Hint values as defined in RFC 7009 Section 2.1
|
||||
*/
|
||||
public enum TokenTypeHint {
|
||||
ACCESS_TOKEN("access_token"),
|
||||
|
||||
REFRESH_TOKEN("refresh_token");
|
||||
|
||||
private final String hint;
|
||||
|
||||
TokenTypeHint(final String hint) {
|
||||
this.hint = hint;
|
||||
}
|
||||
|
||||
public String getHint() {
|
||||
return hint;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.web.authentication;
|
||||
|
||||
import org.apache.nifi.admin.service.IdpUserGroupService;
|
||||
import org.apache.nifi.authorization.util.IdentityMapping;
|
||||
import org.apache.nifi.authorization.util.IdentityMappingUtil;
|
||||
import org.apache.nifi.idp.IdpType;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieService;
|
||||
import org.apache.nifi.web.security.cookie.StandardApplicationCookieService;
|
||||
import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider;
|
||||
import org.apache.nifi.web.security.oidc.OidcConfigurationException;
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||
import org.apache.nifi.web.util.RequestUriBuilder;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* OpenID Connect Authentication Success Handler redirects to the user interface and sets a Session Cookie with a Bearer Token
|
||||
*/
|
||||
public class OidcAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
|
||||
private static final String UI_PATH = "/nifi/";
|
||||
|
||||
private static final String ROOT_PATH = "/";
|
||||
|
||||
private final ApplicationCookieService applicationCookieService = new StandardApplicationCookieService();
|
||||
|
||||
private final BearerTokenProvider bearerTokenProvider;
|
||||
|
||||
private final IdpUserGroupService idpUserGroupService;
|
||||
|
||||
private final List<IdentityMapping> userIdentityMappings;
|
||||
|
||||
private final List<IdentityMapping> groupIdentityMappings;
|
||||
|
||||
private final List<String> userClaimNames;
|
||||
|
||||
private final String groupsClaimName;
|
||||
|
||||
/**
|
||||
* OpenID Connect Authentication Success Handler requires Bearer Token Provider and expiration for generated tokens
|
||||
*
|
||||
* @param bearerTokenProvider Bearer Token Provider
|
||||
* @param idpUserGroupService User Group Service for persisting groups from the Identity Provider
|
||||
* @param userIdentityMappings User Identity Mappings
|
||||
* @param groupIdentityMappings Group Identity Mappings
|
||||
* @param userClaimNames Claim Names for User Identity
|
||||
* @param groupsClaimName Claim Name for Groups contained in ID Token or null when not configured
|
||||
*/
|
||||
public OidcAuthenticationSuccessHandler(
|
||||
final BearerTokenProvider bearerTokenProvider,
|
||||
final IdpUserGroupService idpUserGroupService,
|
||||
final List<IdentityMapping> userIdentityMappings,
|
||||
final List<IdentityMapping> groupIdentityMappings,
|
||||
final List<String> userClaimNames,
|
||||
final String groupsClaimName
|
||||
) {
|
||||
this.bearerTokenProvider = Objects.requireNonNull(bearerTokenProvider, "Bearer Token Provider required");
|
||||
this.idpUserGroupService = Objects.requireNonNull(idpUserGroupService, "User Group Service required");
|
||||
this.userIdentityMappings = Objects.requireNonNull(userIdentityMappings, "User Identity Mappings required");
|
||||
this.groupIdentityMappings = Objects.requireNonNull(groupIdentityMappings, "Group Identity Mappings required");
|
||||
this.userClaimNames = Objects.requireNonNull(userClaimNames, "User Claim Names required");
|
||||
this.groupsClaimName = groupsClaimName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine Redirect Target URL based on Request URL and add Session Cookie containing a Bearer Token
|
||||
*
|
||||
* @param request HTTP Servlet Request
|
||||
* @param response HTTP Servlet Response
|
||||
* @param authentication OpenID Connect Authentication
|
||||
* @return Redirect Target URL
|
||||
*/
|
||||
@Override
|
||||
public String determineTargetUrl(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) {
|
||||
final URI resourceUri = RequestUriBuilder.fromHttpServletRequest(request).path(ROOT_PATH).build();
|
||||
processAuthentication(response, authentication, resourceUri);
|
||||
|
||||
final URI targetUri = RequestUriBuilder.fromHttpServletRequest(request).path(UI_PATH).build();
|
||||
return targetUri.toString();
|
||||
}
|
||||
|
||||
private void processAuthentication(final HttpServletResponse response, final Authentication authentication, final URI resourceUri) {
|
||||
final OAuth2AuthenticationToken authenticationToken = getAuthenticationToken(authentication);
|
||||
final OidcUser oidcUser = getOidcUser(authenticationToken);
|
||||
final String identity = getIdentity(oidcUser);
|
||||
final Set<String> groups = getGroups(oidcUser);
|
||||
idpUserGroupService.replaceUserGroups(identity, IdpType.OIDC, groups);
|
||||
|
||||
final OAuth2AccessToken accessToken = getAccessToken(authenticationToken);
|
||||
final String bearerToken = getBearerToken(identity, oidcUser, accessToken);
|
||||
applicationCookieService.addSessionCookie(resourceUri, response, ApplicationCookieName.AUTHORIZATION_BEARER, bearerToken);
|
||||
}
|
||||
|
||||
private String getBearerToken(final String identity, final OidcUser oidcUser, final OAuth2AccessToken accessToken) {
|
||||
final long sessionExpiration = getSessionExpiration(accessToken);
|
||||
final String issuer = oidcUser.getIssuer().toString();
|
||||
final LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(identity, identity, sessionExpiration, issuer);
|
||||
return bearerTokenProvider.getBearerToken(loginAuthenticationToken);
|
||||
}
|
||||
|
||||
private long getSessionExpiration(final OAuth2Token token) {
|
||||
final Instant tokenExpiration = token.getExpiresAt();
|
||||
if (tokenExpiration == null) {
|
||||
throw new IllegalArgumentException("Token expiration claim not found");
|
||||
}
|
||||
final Instant tokenIssued = token.getIssuedAt();
|
||||
if (tokenIssued == null) {
|
||||
throw new IllegalArgumentException("Token issued claim not found");
|
||||
}
|
||||
final Duration expiration = Duration.between(tokenIssued, tokenExpiration);
|
||||
return expiration.toMillis();
|
||||
}
|
||||
|
||||
private OAuth2AuthenticationToken getAuthenticationToken(final Authentication authentication) {
|
||||
if (authentication instanceof OAuth2AuthenticationToken) {
|
||||
return (OAuth2AuthenticationToken) authentication;
|
||||
} else {
|
||||
final String message = String.format("OAuth2AuthenticationToken not found [%s]", authentication.getClass());
|
||||
throw new IllegalArgumentException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private OAuth2AccessToken getAccessToken(final OAuth2AuthenticationToken authenticationToken) {
|
||||
final Object credentials = authenticationToken.getCredentials();
|
||||
if (credentials instanceof OAuth2AccessToken) {
|
||||
return (OAuth2AccessToken) credentials;
|
||||
} else {
|
||||
final String message = String.format("OAuth2AccessToken not found in credentials [%s]", credentials.getClass());
|
||||
throw new IllegalArgumentException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private OidcUser getOidcUser(final OAuth2AuthenticationToken authenticationToken) {
|
||||
final OAuth2User principalUser = authenticationToken.getPrincipal();
|
||||
if (principalUser instanceof OidcUser) {
|
||||
return (OidcUser) principalUser;
|
||||
} else {
|
||||
final String message = String.format("OpenID Connect User not found [%s]", principalUser.getClass());
|
||||
throw new IllegalArgumentException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private String getIdentity(final OidcUser oidcUser) {
|
||||
final Optional<String> userNameFound = userClaimNames.stream()
|
||||
.map(oidcUser::getClaimAsString)
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst();
|
||||
final String identity = userNameFound.orElseThrow(() -> {
|
||||
final String message = String.format("User Identity not found in Token Claims %s", userClaimNames);
|
||||
return new OidcConfigurationException(message);
|
||||
});
|
||||
return IdentityMappingUtil.mapIdentity(identity, userIdentityMappings);
|
||||
}
|
||||
|
||||
private Set<String> getGroups(final OidcUser oidcUser) {
|
||||
final Set<String> groups;
|
||||
if (groupsClaimName == null || groupsClaimName.isEmpty()) {
|
||||
groups = Collections.emptySet();
|
||||
} else {
|
||||
final List<String> groupsFound = oidcUser.getClaimAsStringList(groupsClaimName);
|
||||
final List<String> claimGroups = groupsFound == null ? Collections.emptyList() : groupsFound;
|
||||
groups = claimGroups.stream()
|
||||
.map(group -> IdentityMappingUtil.mapIdentity(group, groupIdentityMappings))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
}
|
|
@ -16,51 +16,17 @@
|
|||
*/
|
||||
package org.apache.nifi.web.security.saml2.web.authentication.logout;
|
||||
|
||||
import org.apache.nifi.web.security.logout.StandardLogoutFilter;
|
||||
import org.apache.nifi.web.security.saml2.SamlUrlPath;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* SAML 2 Logout Filter completes application Logout Requests
|
||||
*/
|
||||
public class Saml2LocalLogoutFilter extends OncePerRequestFilter {
|
||||
private final AntPathRequestMatcher requestMatcher = new AntPathRequestMatcher(SamlUrlPath.LOCAL_LOGOUT_REQUEST.getPath());
|
||||
|
||||
private final LogoutSuccessHandler logoutSuccessHandler;
|
||||
|
||||
public class Saml2LocalLogoutFilter extends StandardLogoutFilter {
|
||||
public Saml2LocalLogoutFilter(
|
||||
final LogoutSuccessHandler logoutSuccessHandler
|
||||
) {
|
||||
this.logoutSuccessHandler = logoutSuccessHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call Logout Success Handler when request path matches
|
||||
*
|
||||
* @param request HTTP Servlet Request
|
||||
* @param response HTTP Servlet Response
|
||||
* @param filterChain Filter Chain
|
||||
* @throws ServletException Thrown on FilterChain.doFilter() failures
|
||||
* @throws IOException Thrown on FilterChain.doFilter() failures
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
|
||||
if (requestMatcher.matches(request)) {
|
||||
final SecurityContext securityContext = SecurityContextHolder.getContext();
|
||||
final Authentication authentication = securityContext.getAuthentication();
|
||||
logoutSuccessHandler.onLogoutSuccess(request, response, authentication);
|
||||
} else {
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
super(new AntPathRequestMatcher(SamlUrlPath.LOCAL_LOGOUT_REQUEST.getPath()), logoutSuccessHandler);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.apache.nifi.web.security.cookie.StandardApplicationCookieService;
|
|||
import org.apache.nifi.web.security.logout.LogoutRequest;
|
||||
import org.apache.nifi.web.security.logout.LogoutRequestManager;
|
||||
import org.apache.nifi.web.security.saml2.SamlUrlPath;
|
||||
import org.apache.nifi.web.security.token.LogoutAuthenticationToken;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.nifi.web.security.saml2.web.authentication.logout;
|
||||
package org.apache.nifi.web.security.token;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
|
@ -22,7 +22,7 @@ import org.springframework.security.core.authority.AuthorityUtils;
|
|||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Logout Authentication Token for processing Logout Requests using Spring Security SAML 2 handlers
|
||||
* Logout Authentication Token for processing Logout Requests using Spring Security handlers
|
||||
*/
|
||||
public class LogoutAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private final String name;
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.util;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class IdentityProviderUtils {
|
||||
|
||||
/**
|
||||
* Generates a value to use as State in an identity provider login sequence. 128 bits is considered cryptographically strong
|
||||
* with current hardware/software, but a Base32 digit needs 5 bits to be fully encoded, so 128 is rounded up to 130. Base32
|
||||
* is chosen because it encodes data with a single case and without including confusing or URI-incompatible characters,
|
||||
* unlike Base64, but is approximately 20% more compact than Base16/hexadecimal
|
||||
*
|
||||
* @return the state value
|
||||
*/
|
||||
public static String generateStateValue() {
|
||||
return new BigInteger(130, new SecureRandom()).toString(32);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a time constant equality check. If either value is null, false is returned.
|
||||
*
|
||||
* @param value1 value1
|
||||
* @param value2 value2
|
||||
* @return if value1 equals value2
|
||||
*/
|
||||
public static boolean timeConstantEqualityCheck(final String value1, final String value2) {
|
||||
if (value1 == null || value2 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return MessageDigest.isEqual(value1.getBytes(StandardCharsets.UTF_8), value2.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
|
@ -1,167 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc
|
||||
|
||||
import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod
|
||||
import com.nimbusds.oauth2.sdk.id.Issuer
|
||||
import com.nimbusds.openid.connect.sdk.SubjectType
|
||||
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata
|
||||
import org.apache.nifi.util.NiFiProperties
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals
|
||||
import static org.junit.jupiter.api.Assertions.assertNull
|
||||
|
||||
class OidcServiceGroovyTest {
|
||||
private static final Logger logger = LoggerFactory.getLogger(OidcServiceGroovyTest.class)
|
||||
|
||||
private static final Map<String, Object> DEFAULT_NIFI_PROPERTIES = [
|
||||
"nifi.security.user.oidc.discovery.url" : "https://localhost/oidc",
|
||||
"nifi.security.user.login.identity.provider" : "provider",
|
||||
"nifi.security.user.knox.url" : "url",
|
||||
"nifi.security.user.oidc.connect.timeout" : "1000",
|
||||
"nifi.security.user.oidc.read.timeout" : "1000",
|
||||
"nifi.security.user.oidc.client.id" : "expected_client_id",
|
||||
"nifi.security.user.oidc.client.secret" : "expected_client_secret",
|
||||
"nifi.security.user.oidc.claim.identifying.user" : "username",
|
||||
"nifi.security.user.oidc.preferred.jwsalgorithm" : ""
|
||||
]
|
||||
|
||||
// Mock collaborators
|
||||
private static NiFiProperties mockNiFiProperties
|
||||
private static StandardOidcIdentityProvider soip
|
||||
|
||||
private static final String MOCK_REQUEST_IDENTIFIER = "mock-request-identifier"
|
||||
private static final String MOCK_JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" +
|
||||
".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik5pRmkgT0lEQyBVbml0IFRlc3Rlci" +
|
||||
"IsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MzM5MDIyLCJpc3MiOiJuaWZpX3Vua" +
|
||||
"XRfdGVzdF9hdXRob3JpdHkiLCJhdWQiOiJhbGwiLCJ1c2VybmFtZSI6Im9pZGNfdGVzd" +
|
||||
"CIsImVtYWlsIjoib2lkY190ZXN0QG5pZmkuYXBhY2hlLm9yZyJ9" +
|
||||
".b4NIl0RONKdVLOH0D1eObdwAEX8qX-ExqB8KuKSZFLw"
|
||||
|
||||
@BeforeAll
|
||||
static void setUpOnce() throws Exception {
|
||||
logger.metaClass.methodMissing = { String name, args ->
|
||||
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
mockNiFiProperties = buildNiFiProperties()
|
||||
soip = new StandardOidcIdentityProvider(mockNiFiProperties)
|
||||
}
|
||||
|
||||
private static NiFiProperties buildNiFiProperties(Map<String, Object> props = [:]) {
|
||||
def combinedProps = DEFAULT_NIFI_PROPERTIES + props
|
||||
new NiFiProperties(combinedProps)
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldStoreJwt() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockInitializedProvider([:])
|
||||
|
||||
OidcService service = new OidcService(soip)
|
||||
|
||||
// Expected JWT
|
||||
logger.info("EXPECTED_JWT: ${MOCK_JWT}")
|
||||
|
||||
// Act
|
||||
service.storeJwt(MOCK_REQUEST_IDENTIFIER, MOCK_JWT)
|
||||
|
||||
// Assert
|
||||
final String cachedJwt = service.getJwt(MOCK_REQUEST_IDENTIFIER)
|
||||
logger.info("Cached JWT: ${cachedJwt}")
|
||||
|
||||
assertEquals(MOCK_JWT, cachedJwt)
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldGetJwt() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockInitializedProvider([:])
|
||||
|
||||
OidcService service = new OidcService(soip)
|
||||
|
||||
// Expected JWT
|
||||
logger.info("EXPECTED_JWT: ${MOCK_JWT}")
|
||||
|
||||
// store the jwt
|
||||
service.storeJwt(MOCK_REQUEST_IDENTIFIER, MOCK_JWT)
|
||||
|
||||
// Act
|
||||
final String retrievedJwt = service.getJwt(MOCK_REQUEST_IDENTIFIER)
|
||||
logger.info("Retrieved JWT: ${retrievedJwt}")
|
||||
|
||||
// Assert
|
||||
assertEquals(MOCK_JWT, retrievedJwt)
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetJwtShouldReturnNullWithExpiredDuration() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockInitializedProvider([:])
|
||||
|
||||
final int DURATION = 500
|
||||
final TimeUnit EXPIRATION_UNITS = TimeUnit.MILLISECONDS
|
||||
OidcService service = new OidcService(soip, DURATION, EXPIRATION_UNITS)
|
||||
|
||||
// Expected JWT
|
||||
logger.info("EXPECTED_JWT: ${MOCK_JWT}")
|
||||
|
||||
// Store the jwt
|
||||
service.storeJwt(MOCK_REQUEST_IDENTIFIER, MOCK_JWT)
|
||||
|
||||
// Put thread to sleep
|
||||
long millis = 1000
|
||||
Thread.sleep(millis)
|
||||
logger.info("Thread will sleep for: ${millis} ms")
|
||||
|
||||
// Act
|
||||
final String retrievedJwt = service.getJwt(MOCK_REQUEST_IDENTIFIER)
|
||||
logger.info("Retrieved JWT: ${retrievedJwt}")
|
||||
|
||||
// Assert
|
||||
assertNull(retrievedJwt)
|
||||
}
|
||||
|
||||
private static StandardOidcIdentityProvider buildIdentityProviderWithMockInitializedProvider(Map<String, String> additionalProperties = [:]) {
|
||||
NiFiProperties mockNFP = buildNiFiProperties(additionalProperties)
|
||||
|
||||
// Mock OIDC provider metadata
|
||||
Issuer mockIssuer = new Issuer("mockIssuer")
|
||||
URI mockURI = new URI("https://localhost/oidc")
|
||||
OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI)
|
||||
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNFP) {
|
||||
@Override
|
||||
void initializeProvider() {
|
||||
soip.oidcProviderMetadata = metadata
|
||||
soip.oidcProviderMetadata["tokenEndpointAuthMethods"] = [ClientAuthenticationMethod.CLIENT_SECRET_BASIC]
|
||||
soip.oidcProviderMetadata["userInfoEndpointURI"] = new URI("https://localhost/oidc/token")
|
||||
}
|
||||
}
|
||||
soip
|
||||
}
|
||||
}
|
|
@ -1,732 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc
|
||||
|
||||
import com.nimbusds.jose.JWSAlgorithm
|
||||
import com.nimbusds.jwt.JWT
|
||||
import com.nimbusds.jwt.JWTClaimsSet
|
||||
import com.nimbusds.jwt.PlainJWT
|
||||
import com.nimbusds.oauth2.sdk.AuthorizationCode
|
||||
import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant
|
||||
import com.nimbusds.oauth2.sdk.auth.ClientAuthentication
|
||||
import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod
|
||||
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic
|
||||
import com.nimbusds.oauth2.sdk.auth.ClientSecretPost
|
||||
import com.nimbusds.oauth2.sdk.auth.Secret
|
||||
import com.nimbusds.oauth2.sdk.http.HTTPRequest
|
||||
import com.nimbusds.oauth2.sdk.http.HTTPResponse
|
||||
import com.nimbusds.oauth2.sdk.id.ClientID
|
||||
import com.nimbusds.oauth2.sdk.id.Issuer
|
||||
import com.nimbusds.oauth2.sdk.token.AccessToken
|
||||
import com.nimbusds.oauth2.sdk.token.BearerAccessToken
|
||||
import com.nimbusds.oauth2.sdk.token.RefreshToken
|
||||
import com.nimbusds.openid.connect.sdk.Nonce
|
||||
import com.nimbusds.openid.connect.sdk.OIDCTokenResponse
|
||||
import com.nimbusds.openid.connect.sdk.SubjectType
|
||||
import com.nimbusds.openid.connect.sdk.claims.AccessTokenHash
|
||||
import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet
|
||||
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata
|
||||
import com.nimbusds.openid.connect.sdk.token.OIDCTokens
|
||||
import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator
|
||||
import groovy.json.JsonOutput
|
||||
import net.minidev.json.JSONObject
|
||||
import org.apache.commons.codec.binary.Base64
|
||||
import org.apache.nifi.util.NiFiProperties
|
||||
import org.apache.nifi.util.StringUtils
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import static org.junit.jupiter.api.Assertions.assertNull
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue
|
||||
|
||||
class StandardOidcIdentityProviderGroovyTest {
|
||||
private static final Logger logger = LoggerFactory.getLogger(StandardOidcIdentityProviderGroovyTest.class)
|
||||
|
||||
private static final Map<String, Object> DEFAULT_NIFI_PROPERTIES = [
|
||||
"nifi.security.user.oidc.discovery.url" : "https://localhost/oidc",
|
||||
"nifi.security.user.login.identity.provider" : "provider",
|
||||
"nifi.security.user.knox.url" : "url",
|
||||
"nifi.security.user.oidc.connect.timeout" : "1000",
|
||||
"nifi.security.user.oidc.read.timeout" : "1000",
|
||||
"nifi.security.user.oidc.client.id" : "expected_client_id",
|
||||
"nifi.security.user.oidc.client.secret" : "expected_client_secret",
|
||||
"nifi.security.user.oidc.claim.identifying.user" : "username",
|
||||
"nifi.security.user.oidc.preferred.jwsalgorithm" : ""
|
||||
]
|
||||
|
||||
// Mock collaborators
|
||||
private static NiFiProperties mockNiFiProperties
|
||||
|
||||
private static final String MOCK_JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZ" +
|
||||
"SI6Ik5pRmkgT0lEQyBVbml0IFRlc3RlciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MzM5MDIyLCJpc3MiOiJuaWZp" +
|
||||
"X3VuaXRfdGVzdF9hdXRob3JpdHkiLCJhdWQiOiJhbGwiLCJ1c2VybmFtZSI6Im9pZGNfdGVzdCIsImVtYWlsIjoib2lkY19" +
|
||||
"0ZXN0QG5pZmkuYXBhY2hlLm9yZyJ9.b4NIl0RONKdVLOH0D1eObdwAEX8qX-ExqB8KuKSZFLw"
|
||||
|
||||
@BeforeAll
|
||||
static void setUpOnce() throws Exception {
|
||||
logger.metaClass.methodMissing = { String name, args ->
|
||||
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
mockNiFiProperties = buildNiFiProperties()
|
||||
}
|
||||
|
||||
private static NiFiProperties buildNiFiProperties(Map<String, Object> props = [:]) {
|
||||
def combinedProps = DEFAULT_NIFI_PROPERTIES + props
|
||||
new NiFiProperties(combinedProps)
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldGetAvailableClaims() {
|
||||
// Arrange
|
||||
final Map<String, String> EXPECTED_CLAIMS = [
|
||||
"iss" : "https://accounts.issuer.com",
|
||||
"azp" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.usercontent.com",
|
||||
"aud" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.usercontent.com",
|
||||
"sub" : "10703475345439756345540",
|
||||
"email" : "person@nifi.apache.org",
|
||||
"email_verified": "true",
|
||||
"at_hash" : "JOGISUDHFiyGHDSFwV5Fah2A",
|
||||
"iat" : "1590022674",
|
||||
"exp" : "1590026274",
|
||||
"empty_claim" : ""
|
||||
]
|
||||
|
||||
final List<String> POPULATED_CLAIM_NAMES = EXPECTED_CLAIMS.findAll { k, v -> StringUtils.isNotBlank(v) }.keySet().sort()
|
||||
|
||||
JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(EXPECTED_CLAIMS)
|
||||
|
||||
// Act
|
||||
def definedClaims = StandardOidcIdentityProvider.getAvailableClaims(mockJWTClaimsSet)
|
||||
logger.info("Defined claims: ${definedClaims}")
|
||||
|
||||
// Assert
|
||||
assertEquals(POPULATED_CLAIM_NAMES, definedClaims)
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldCreateClientAuthenticationFromPost() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNiFiProperties)
|
||||
|
||||
Issuer mockIssuer = new Issuer("https://localhost/oidc")
|
||||
URI mockURI = new URI("https://localhost/oidc")
|
||||
OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI)
|
||||
|
||||
soip.oidcProviderMetadata = metadata
|
||||
|
||||
// Set Authorization Method
|
||||
soip.oidcProviderMetadata["tokenEndpointAuthMethods"] = [ClientAuthenticationMethod.CLIENT_SECRET_POST]
|
||||
final List<ClientAuthenticationMethod> mockAuthMethod = soip.oidcProviderMetadata["tokenEndpointAuthMethods"]
|
||||
logger.info("Provided Auth Method: ${mockAuthMethod}")
|
||||
|
||||
// Define expected values
|
||||
final ClientID CLIENT_ID = new ClientID("expected_client_id")
|
||||
final Secret CLIENT_SECRET = new Secret("expected_client_secret")
|
||||
|
||||
// Inject into OIP
|
||||
soip.clientId = CLIENT_ID
|
||||
soip.clientSecret = CLIENT_SECRET
|
||||
|
||||
final ClientAuthentication EXPECTED_CLIENT_AUTHENTICATION = new ClientSecretPost(CLIENT_ID, CLIENT_SECRET)
|
||||
|
||||
// Act
|
||||
def clientAuthentication = soip.createClientAuthentication()
|
||||
logger.info("Client Auth properties: ${clientAuthentication.getProperties()}")
|
||||
|
||||
// Assert
|
||||
assertEquals(EXPECTED_CLIENT_AUTHENTICATION.getClientID(), clientAuthentication.getClientID())
|
||||
logger.info("Client secret: ${(clientAuthentication as ClientSecretPost).clientSecret.value}")
|
||||
assertEquals(((ClientSecretPost) EXPECTED_CLIENT_AUTHENTICATION).getClientSecret(), ((ClientSecretPost) clientAuthentication).getClientSecret())
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldCreateClientAuthenticationFromBasic() {
|
||||
// Arrange
|
||||
// Mock collaborators
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNiFiProperties)
|
||||
|
||||
Issuer mockIssuer = new Issuer("https://localhost/oidc")
|
||||
URI mockURI = new URI("https://localhost/oidc")
|
||||
OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI)
|
||||
soip.oidcProviderMetadata = metadata
|
||||
|
||||
// Set Auth Method
|
||||
soip.oidcProviderMetadata["tokenEndpointAuthMethods"] = [ClientAuthenticationMethod.CLIENT_SECRET_BASIC]
|
||||
final List<ClientAuthenticationMethod> mockAuthMethod = soip.oidcProviderMetadata["tokenEndpointAuthMethods"]
|
||||
logger.info("Provided Auth Method: ${mockAuthMethod}")
|
||||
|
||||
// Define expected values
|
||||
final ClientID CLIENT_ID = new ClientID("expected_client_id")
|
||||
final Secret CLIENT_SECRET = new Secret("expected_client_secret")
|
||||
|
||||
// Inject into OIP
|
||||
soip.clientId = CLIENT_ID
|
||||
soip.clientSecret = CLIENT_SECRET
|
||||
|
||||
final ClientAuthentication EXPECTED_CLIENT_AUTHENTICATION = new ClientSecretBasic(CLIENT_ID, CLIENT_SECRET)
|
||||
|
||||
// Act
|
||||
def clientAuthentication = soip.createClientAuthentication()
|
||||
logger.info("Client authentication properties: ${clientAuthentication.properties}")
|
||||
|
||||
// Assert
|
||||
assertEquals(EXPECTED_CLIENT_AUTHENTICATION.getClientID(), clientAuthentication.getClientID())
|
||||
assertEquals(EXPECTED_CLIENT_AUTHENTICATION.getMethod(), clientAuthentication.getMethod())
|
||||
logger.info("Client secret: ${(clientAuthentication as ClientSecretBasic).clientSecret.value}")
|
||||
assertEquals(EXPECTED_CLIENT_AUTHENTICATION.clientSecret, (clientAuthentication as ClientSecretBasic).getClientSecret())
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldCreateTokenHTTPRequest() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNiFiProperties)
|
||||
|
||||
// Mock AuthorizationGrant
|
||||
Issuer mockIssuer = new Issuer("https://localhost/oidc")
|
||||
URI mockURI = new URI("https://localhost/oidc")
|
||||
AuthorizationCode mockCode = new AuthorizationCode("ABCDE")
|
||||
def mockAuthGrant = new AuthorizationCodeGrant(mockCode, mockURI)
|
||||
|
||||
OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI)
|
||||
soip.oidcProviderMetadata = metadata
|
||||
|
||||
// Set OIDC Provider metadata attributes
|
||||
final ClientID CLIENT_ID = new ClientID("expected_client_id")
|
||||
final Secret CLIENT_SECRET = new Secret("expected_client_secret")
|
||||
|
||||
// Inject into OIP
|
||||
soip.clientId = CLIENT_ID
|
||||
soip.clientSecret = CLIENT_SECRET
|
||||
soip.oidcProviderMetadata["tokenEndpointAuthMethods"] = [ClientAuthenticationMethod.CLIENT_SECRET_BASIC]
|
||||
soip.oidcProviderMetadata["tokenEndpointURI"] = new URI("https://localhost/token")
|
||||
|
||||
// Mock ClientAuthentication
|
||||
def clientAuthentication = soip.createClientAuthentication()
|
||||
|
||||
// Act
|
||||
def httpRequest = soip.createTokenHTTPRequest(mockAuthGrant, clientAuthentication)
|
||||
logger.info("HTTP Request: ${httpRequest.dump()}")
|
||||
logger.info("Query: ${URLDecoder.decode(httpRequest.query, "UTF-8")}")
|
||||
|
||||
// Assert
|
||||
assertEquals("POST", httpRequest.getMethod().name())
|
||||
assertTrue(httpRequest.query.contains("code=" + mockCode.value))
|
||||
String encodedUri = URLEncoder.encode("https://localhost/oidc", "UTF-8")
|
||||
assertTrue(httpRequest.query.contains("redirect_uri="+encodedUri+"&grant_type=authorization_code"))
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldLookupIdentityInUserInfo() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNiFiProperties)
|
||||
|
||||
Issuer mockIssuer = new Issuer("https://localhost/oidc")
|
||||
URI mockURI = new URI("https://localhost/oidc")
|
||||
|
||||
OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI)
|
||||
soip.oidcProviderMetadata = metadata
|
||||
|
||||
final String EXPECTED_IDENTITY = "my_username"
|
||||
|
||||
def responseBody = [username: EXPECTED_IDENTITY, sub: "testSub"]
|
||||
HTTPRequest mockUserInfoRequest = mockHttpRequest(responseBody, 200, "HTTP OK")
|
||||
|
||||
// Act
|
||||
String identity = soip.lookupIdentityInUserInfo(mockUserInfoRequest)
|
||||
logger.info("Identity: ${identity}")
|
||||
|
||||
// Assert
|
||||
assertEquals(EXPECTED_IDENTITY, identity)
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLookupIdentityUserInfoShouldHandleMissingIdentity() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNiFiProperties)
|
||||
|
||||
Issuer mockIssuer = new Issuer("https://localhost/oidc")
|
||||
URI mockURI = new URI("https://localhost/oidc")
|
||||
|
||||
OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI)
|
||||
soip.oidcProviderMetadata = metadata
|
||||
|
||||
def responseBody = [username: "", sub: "testSub"]
|
||||
HTTPRequest mockUserInfoRequest = mockHttpRequest(responseBody, 200, "HTTP NO USER")
|
||||
|
||||
// Act
|
||||
IllegalStateException ise = assertThrows(IllegalStateException.class, () -> soip.lookupIdentityInUserInfo(mockUserInfoRequest))
|
||||
assertTrue(ise.getMessage().contains("Unable to extract identity from the UserInfo token using the claim 'username'."))
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLookupIdentityUserInfoShouldHandle500() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNiFiProperties)
|
||||
|
||||
Issuer mockIssuer = new Issuer("https://localhost/oidc")
|
||||
URI mockURI = new URI("https://localhost/oidc")
|
||||
|
||||
OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI)
|
||||
soip.oidcProviderMetadata = metadata
|
||||
|
||||
def errorBody = [error : "Failure to authenticate",
|
||||
error_description: "The provided username and password were not correct",
|
||||
error_uri : "https://localhost/oidc/error"]
|
||||
HTTPRequest mockUserInfoRequest = mockHttpRequest(errorBody, 500, "HTTP ERROR")
|
||||
|
||||
RuntimeException re = assertThrows(RuntimeException.class, () -> soip.lookupIdentityInUserInfo(mockUserInfoRequest))
|
||||
assertTrue(re.getMessage().contains("An error occurred while invoking the UserInfo endpoint: The provided username and password " +
|
||||
"were not correct"))
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldConvertOIDCTokenToLoginAuthenticationToken() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "email"])
|
||||
|
||||
OIDCTokenResponse mockResponse = mockOIDCTokenResponse()
|
||||
logger.info("OIDC Token Response: ${mockResponse.dump()}")
|
||||
|
||||
// Act
|
||||
final String loginToken = soip.convertOIDCTokenToLoginAuthenticationToken(mockResponse)
|
||||
logger.info("Login Authentication token: ${loginToken}")
|
||||
|
||||
// Assert
|
||||
// Split ID Token into components
|
||||
def (String contents, String expiration) = loginToken.tokenize("\\[\\]")
|
||||
logger.info("Token contents: ${contents} | Expiration: ${expiration}")
|
||||
|
||||
assertTrue(contents.contains("LoginAuthenticationToken for person@nifi.apache.org issued by https://accounts.issuer.com expiring at"))
|
||||
|
||||
// Assert expiration
|
||||
final String[] expList = expiration.split("\\s")
|
||||
final Long expLong = Long.parseLong(expList[0])
|
||||
assertTrue(expLong <= System.currentTimeMillis() + 10_000)
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertOIDCTokenToLoginAuthenticationTokenShouldHandleBlankIdentity() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "non-existent-claim"])
|
||||
|
||||
OIDCTokenResponse mockResponse = mockOIDCTokenResponse()
|
||||
logger.info("OIDC Token Response: ${mockResponse.dump()}")
|
||||
|
||||
// Act
|
||||
String loginToken = soip.convertOIDCTokenToLoginAuthenticationToken(mockResponse)
|
||||
logger.info("Login Authentication token: ${loginToken}")
|
||||
|
||||
// Assert
|
||||
// Split ID Token into components
|
||||
def (String contents, String expiration) = loginToken.tokenize("\\[\\]")
|
||||
logger.info("Token contents: ${contents} | Expiration: ${expiration}")
|
||||
|
||||
assertTrue(contents.contains("LoginAuthenticationToken for person@nifi.apache.org issued by https://accounts.issuer.com expiring at"))
|
||||
|
||||
// Get the expiration
|
||||
final ArrayList<String> expires = expiration.split("[\\D*]")
|
||||
final long exp = Long.parseLong(expires[0])
|
||||
logger.info("exp: ${exp} ms")
|
||||
|
||||
assertTrue(exp <= System.currentTimeMillis() + 10_000)
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertOIDCTokenToLoginAuthenticationTokenShouldHandleNoEmailClaimHasFallbackClaims() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(
|
||||
["nifi.security.user.oidc.claim.identifying.user": "email",
|
||||
"nifi.security.user.oidc.fallback.claims.identifying.user": "upn" ])
|
||||
String expectedUpn = "xxx@aaddomain"
|
||||
|
||||
OIDCTokenResponse mockResponse = mockOIDCTokenResponse(["email": null, "upn": expectedUpn])
|
||||
logger.info("OIDC Token Response with no email and upn: ${mockResponse.dump()}")
|
||||
|
||||
String loginToken = soip.convertOIDCTokenToLoginAuthenticationToken(mockResponse)
|
||||
logger.info("NiFi token create with upn: ${loginToken}")
|
||||
// Assert
|
||||
// Split JWT into components and decode Base64 to JSON
|
||||
def (String contents, String expiration) = loginToken.tokenize("\\[\\]")
|
||||
logger.info("Token contents: ${contents} | Expiration: ${expiration}")
|
||||
assertTrue(contents.contains("LoginAuthenticationToken for " + expectedUpn + " issued by https://accounts.issuer.com expiring at"))
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAuthorizeClientRequestShouldHandleError() {
|
||||
// Arrange
|
||||
// Build ID Provider with mock token endpoint URI to make a connection
|
||||
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator([:])
|
||||
|
||||
def responseBody = [id_token: MOCK_JWT, access_token: "some.access.token", refresh_token: "some.refresh.token", token_type: "bearer"]
|
||||
HTTPRequest mockTokenRequest = mockHttpRequest(responseBody, 500, "HTTP SERVER ERROR")
|
||||
|
||||
// Act
|
||||
RuntimeException re = assertThrows(RuntimeException.class, () -> soip.authorizeClientRequest(mockTokenRequest))
|
||||
// Assert
|
||||
assertTrue(re.getMessage().contains("An error occurred while invoking the Token endpoint: null"))
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertOIDCTokenToLoginAuthNTokenShouldHandleBlankIdentityAndNoEmailClaim() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "non-existent-claim", "getOidcFallbackClaimsIdentifyingUser": [] ])
|
||||
|
||||
OIDCTokenResponse mockResponse = mockOIDCTokenResponse(["email": null])
|
||||
logger.info("OIDC Token Response: ${mockResponse.dump()}")
|
||||
|
||||
// Act
|
||||
assertThrows(IOException.class, () -> soip.convertOIDCTokenToLoginAuthenticationToken(mockResponse))
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldAuthorizeClientRequest() {
|
||||
// Arrange
|
||||
// Build ID Provider with mock token endpoint URI to make a connection
|
||||
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator([:])
|
||||
|
||||
def responseBody = [id_token: MOCK_JWT, access_token: "some.access.token", refresh_token: "some.refresh.token", token_type: "bearer"]
|
||||
HTTPRequest mockTokenRequest = mockHttpRequest(responseBody, 200, "HTTP OK")
|
||||
|
||||
// Act
|
||||
def tokenResponse = soip.authorizeClientRequest(mockTokenRequest)
|
||||
logger.info("Token Response: ${tokenResponse.dump()}")
|
||||
|
||||
// Assert
|
||||
assertNotNull(tokenResponse)
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldGetAccessTokenString() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator()
|
||||
|
||||
// Mock access tokens
|
||||
AccessToken mockAccessToken = new BearerAccessToken()
|
||||
RefreshToken mockRefreshToken = new RefreshToken()
|
||||
|
||||
// Compute the access token hash
|
||||
final JWSAlgorithm jwsAlgorithm = JWSAlgorithm.RS256
|
||||
AccessTokenHash EXPECTED_HASH = AccessTokenHash.compute(mockAccessToken, jwsAlgorithm)
|
||||
logger.info("Expected at_hash: ${EXPECTED_HASH}")
|
||||
|
||||
// Create mock claims with at_hash
|
||||
Map<String, Object> mockClaims = (["at_hash": EXPECTED_HASH.toString()])
|
||||
|
||||
// Create Claims Set
|
||||
JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(mockClaims)
|
||||
|
||||
// Create JWT
|
||||
JWT mockJwt = new PlainJWT(mockJWTClaimsSet)
|
||||
|
||||
// Create OIDC Tokens
|
||||
OIDCTokens mockOidcTokens = new OIDCTokens(mockJwt, mockAccessToken, mockRefreshToken)
|
||||
|
||||
// Create OIDC Token Response
|
||||
OIDCTokenResponse mockResponse = new OIDCTokenResponse(mockOidcTokens)
|
||||
|
||||
// Act
|
||||
String accessTokenString = soip.getAccessTokenString(mockResponse)
|
||||
logger.info("Access token: ${accessTokenString}")
|
||||
|
||||
// Assert
|
||||
assertNotNull(accessTokenString)
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldValidateAccessToken() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator()
|
||||
|
||||
// Mock access tokens
|
||||
AccessToken mockAccessToken = new BearerAccessToken()
|
||||
logger.info("mock access token: ${mockAccessToken.toString()}")
|
||||
RefreshToken mockRefreshToken = new RefreshToken()
|
||||
|
||||
// Compute the access token hash
|
||||
final JWSAlgorithm jwsAlgorithm = JWSAlgorithm.RS256
|
||||
AccessTokenHash EXPECTED_HASH = AccessTokenHash.compute(mockAccessToken, jwsAlgorithm)
|
||||
logger.info("Expected at_hash: ${EXPECTED_HASH}")
|
||||
|
||||
// Create mock claim
|
||||
final Map<String, Object> claims = mockClaims(["at_hash":EXPECTED_HASH.toString()])
|
||||
|
||||
// Create Claims Set
|
||||
JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(claims)
|
||||
|
||||
// Create JWT
|
||||
JWT mockJwt = new PlainJWT(mockJWTClaimsSet)
|
||||
|
||||
// Create OIDC Tokens
|
||||
OIDCTokens mockOidcTokens = new OIDCTokens(mockJwt, mockAccessToken, mockRefreshToken)
|
||||
logger.info("mock id tokens: ${mockOidcTokens.getIDToken().properties}")
|
||||
|
||||
// Act
|
||||
String accessTokenString = soip.validateAccessToken(mockOidcTokens)
|
||||
logger.info("Access Token: ${accessTokenString}")
|
||||
|
||||
// Assert
|
||||
assertNull(accessTokenString)
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValidateAccessTokenShouldHandleMismatchedATHash() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator()
|
||||
|
||||
// Mock access tokens
|
||||
AccessToken mockAccessToken = new BearerAccessToken()
|
||||
RefreshToken mockRefreshToken = new RefreshToken()
|
||||
|
||||
// Compute the access token hash
|
||||
final JWSAlgorithm jwsAlgorithm = JWSAlgorithm.RS256
|
||||
AccessTokenHash expectedHash = AccessTokenHash.compute(mockAccessToken, jwsAlgorithm)
|
||||
logger.info("Expected at_hash: ${expectedHash}")
|
||||
|
||||
// Create mock claim with incorrect 'at_hash'
|
||||
final Map<String, Object> claims = mockClaims()
|
||||
|
||||
// Create Claims Set
|
||||
JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(claims)
|
||||
|
||||
// Create JWT
|
||||
JWT mockJwt = new PlainJWT(mockJWTClaimsSet)
|
||||
|
||||
// Create OIDC Tokens
|
||||
OIDCTokens mockOidcTokens = new OIDCTokens(mockJwt, mockAccessToken, mockRefreshToken)
|
||||
|
||||
// Act
|
||||
Exception e = assertThrows(Exception.class, () -> soip.validateAccessToken(mockOidcTokens))
|
||||
// Assert
|
||||
assertTrue(e.getMessage().contains("Unable to validate the Access Token: Access token hash (at_hash) mismatch"))
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldGetIdTokenString() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator()
|
||||
|
||||
// Mock access tokens
|
||||
AccessToken mockAccessToken = new BearerAccessToken()
|
||||
RefreshToken mockRefreshToken = new RefreshToken()
|
||||
|
||||
// Create mock claim
|
||||
final Map<String, Object> claims = mockClaims()
|
||||
|
||||
// Create Claims Set
|
||||
JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(claims)
|
||||
|
||||
// Create JWT
|
||||
JWT mockJwt = new PlainJWT(mockJWTClaimsSet)
|
||||
|
||||
// Create OIDC Tokens
|
||||
OIDCTokens mockOidcTokens = new OIDCTokens(mockJwt, mockAccessToken, mockRefreshToken)
|
||||
|
||||
final String EXPECTED_ID_TOKEN = mockOidcTokens.getIDTokenString()
|
||||
logger.info("EXPECTED_ID_TOKEN: ${EXPECTED_ID_TOKEN}")
|
||||
|
||||
// Create OIDC Token Response
|
||||
OIDCTokenResponse mockResponse = new OIDCTokenResponse(mockOidcTokens)
|
||||
|
||||
// Act
|
||||
final String idTokenString = soip.getIdTokenString(mockResponse)
|
||||
logger.info("ID Token: ${idTokenString}")
|
||||
|
||||
// Assert
|
||||
assertNotNull(idTokenString)
|
||||
assertEquals(EXPECTED_ID_TOKEN, idTokenString)
|
||||
|
||||
// Assert that 'email:person@nifi.apache.org' is present
|
||||
def (String header, String payload) = idTokenString.split("\\.")
|
||||
final byte[] idTokenBytes = Base64.decodeBase64(payload)
|
||||
final String payloadString = new String(idTokenBytes, "UTF-8")
|
||||
logger.info(payloadString)
|
||||
|
||||
assertTrue(payloadString.contains("\"email\":\"person@nifi.apache.org\""))
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldValidateIdToken() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator()
|
||||
|
||||
// Create mock claim
|
||||
final Map<String, Object> claims = mockClaims()
|
||||
|
||||
// Create Claims Set
|
||||
JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(claims)
|
||||
|
||||
// Create JWT
|
||||
JWT mockJwt = new PlainJWT(mockJWTClaimsSet)
|
||||
|
||||
// Act
|
||||
final IDTokenClaimsSet claimsSet = soip.validateIdToken(mockJwt)
|
||||
final String claimsSetString = claimsSet.toJSONObject().toString()
|
||||
logger.info("ID Token Claims Set: ${claimsSetString}")
|
||||
|
||||
// Assert
|
||||
assertNotNull(claimsSet)
|
||||
assertTrue(claimsSetString.contains("\"email\":\"person@nifi.apache.org\""))
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldGetOidcTokens() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator()
|
||||
|
||||
// Mock access tokens
|
||||
AccessToken mockAccessToken = new BearerAccessToken()
|
||||
RefreshToken mockRefreshToken = new RefreshToken()
|
||||
|
||||
// Create mock claim
|
||||
final Map<String, Object> claims = mockClaims()
|
||||
|
||||
// Create Claims Set
|
||||
JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(claims)
|
||||
|
||||
// Create JWT
|
||||
JWT mockJwt = new PlainJWT(mockJWTClaimsSet)
|
||||
|
||||
// Create OIDC Tokens
|
||||
OIDCTokens mockOidcTokens = new OIDCTokens(mockJwt, mockAccessToken, mockRefreshToken)
|
||||
|
||||
final String EXPECTED_ID_TOKEN = mockOidcTokens.getIDTokenString()
|
||||
logger.info("EXPECTED_ID_TOKEN: ${EXPECTED_ID_TOKEN}")
|
||||
|
||||
// Create OIDC Token Response
|
||||
OIDCTokenResponse mockResponse = new OIDCTokenResponse(mockOidcTokens)
|
||||
|
||||
// Act
|
||||
final OIDCTokens oidcTokens = soip.getOidcTokens(mockResponse)
|
||||
logger.info("OIDC Tokens: ${oidcTokens.toJSONObject()}")
|
||||
|
||||
// Assert
|
||||
assertNotNull(oidcTokens)
|
||||
|
||||
// Assert ID Tokens match
|
||||
final JSONObject oidcJson = oidcTokens.toJSONObject()
|
||||
final String idToken = oidcJson["id_token"]
|
||||
logger.info("ID Token String: ${idToken}")
|
||||
assertEquals(EXPECTED_ID_TOKEN, idToken)
|
||||
}
|
||||
|
||||
private StandardOidcIdentityProvider buildIdentityProviderWithMockTokenValidator(Map<String, String> additionalProperties = [:]) {
|
||||
NiFiProperties mockNFP = buildNiFiProperties(additionalProperties)
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNFP)
|
||||
|
||||
// Mock OIDC provider metadata
|
||||
Issuer mockIssuer = new Issuer("mockIssuer")
|
||||
URI mockURI = new URI("https://localhost/oidc")
|
||||
OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI)
|
||||
soip.oidcProviderMetadata = metadata
|
||||
|
||||
// Set OIDC Provider metadata attributes
|
||||
final ClientID CLIENT_ID = new ClientID("expected_client_id")
|
||||
final Secret CLIENT_SECRET = new Secret("expected_client_secret")
|
||||
|
||||
// Inject into OIP
|
||||
soip.clientId = CLIENT_ID
|
||||
soip.clientSecret = CLIENT_SECRET
|
||||
soip.oidcProviderMetadata["tokenEndpointAuthMethods"] = [ClientAuthenticationMethod.CLIENT_SECRET_BASIC]
|
||||
soip.oidcProviderMetadata["tokenEndpointURI"] = new URI("https://localhost/oidc/token")
|
||||
soip.oidcProviderMetadata["userInfoEndpointURI"] = new URI("https://localhost/oidc/userInfo")
|
||||
|
||||
// Mock token validator
|
||||
IDTokenValidator mockTokenValidator = new IDTokenValidator(mockIssuer, CLIENT_ID) {
|
||||
@Override
|
||||
IDTokenClaimsSet validate(JWT jwt, Nonce nonce) {
|
||||
return new IDTokenClaimsSet(jwt.getJWTClaimsSet())
|
||||
}
|
||||
}
|
||||
soip.tokenValidator = mockTokenValidator
|
||||
soip
|
||||
}
|
||||
|
||||
private OIDCTokenResponse mockOIDCTokenResponse(Map<String, Object> additionalClaims = [:]) {
|
||||
Map<String, Object> claims = mockClaims(additionalClaims)
|
||||
|
||||
// Create Claims Set
|
||||
JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(claims)
|
||||
|
||||
// Create JWT
|
||||
JWT mockJwt = new PlainJWT(mockJWTClaimsSet)
|
||||
|
||||
// Mock access tokens
|
||||
AccessToken mockAccessToken = new BearerAccessToken()
|
||||
RefreshToken mockRefreshToken = new RefreshToken()
|
||||
|
||||
// Create OIDC Tokens
|
||||
OIDCTokens mockOidcTokens = new OIDCTokens(mockJwt, mockAccessToken, mockRefreshToken)
|
||||
|
||||
// Create OIDC Token Response
|
||||
OIDCTokenResponse mockResponse = new OIDCTokenResponse(mockOidcTokens)
|
||||
mockResponse
|
||||
}
|
||||
|
||||
private static Map<String, Object> mockClaims(Map<String, Object> additionalClaims = [:]) {
|
||||
final Map<String, Object> claims = [
|
||||
"iss" : "https://accounts.issuer.com",
|
||||
"azp" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.usercontent.com",
|
||||
"aud" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.usercontent.com",
|
||||
"sub" : "10703475345439756345540",
|
||||
"email" : "person@nifi.apache.org",
|
||||
"email_verified": "true",
|
||||
"at_hash" : "JOGISUDHFiyGHDSFwV5Fah2A",
|
||||
"iat" : 1590022674,
|
||||
"exp" : 1590026274
|
||||
] + additionalClaims
|
||||
claims
|
||||
}
|
||||
|
||||
/**
|
||||
* Forms an {@link HTTPRequest} object which returns a static response when {@code send( )} is called.
|
||||
*
|
||||
* @param body the JSON body in Map form
|
||||
* @param statusCode the HTTP status code
|
||||
* @param status the HTTP status message
|
||||
* @param headers an optional map of HTTP response headers
|
||||
* @param method the HTTP method to mock
|
||||
* @param url the endpoint URL
|
||||
* @return the static HTTP response
|
||||
*/
|
||||
private static HTTPRequest mockHttpRequest(def body,
|
||||
int statusCode = 200,
|
||||
String status = "HTTP Response",
|
||||
Map<String, String> headers = [:],
|
||||
HTTPRequest.Method method = HTTPRequest.Method.GET,
|
||||
URL url = new URL("https://localhost/oidc")) {
|
||||
new HTTPRequest(method, url) {
|
||||
HTTPResponse send() {
|
||||
HTTPResponse mockResponse = new HTTPResponse(statusCode)
|
||||
mockResponse.setStatusMessage(status)
|
||||
(["Content-Type": "application/json"] + headers).each { String h, String v -> mockResponse.setHeader(h, v) }
|
||||
def responseBody = body
|
||||
mockResponse.setContent(JsonOutput.toJson(responseBody))
|
||||
mockResponse
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,157 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc;
|
||||
|
||||
import com.nimbusds.oauth2.sdk.AuthorizationCode;
|
||||
import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
|
||||
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
|
||||
import com.nimbusds.oauth2.sdk.id.State;
|
||||
import java.net.URI;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class OidcServiceTest {
|
||||
|
||||
public static final String TEST_REQUEST_IDENTIFIER = "test-request-identifier";
|
||||
public static final String TEST_STATE = "test-state";
|
||||
|
||||
@Test
|
||||
public void testOidcNotEnabledCreateState() {
|
||||
final OidcService service = getServiceWithNoOidcSupport();
|
||||
assertThrows(IllegalStateException.class, () -> service.createState(TEST_REQUEST_IDENTIFIER));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateStateMultipleInvocations() {
|
||||
final OidcService service = getServiceWithOidcSupport();
|
||||
service.createState(TEST_REQUEST_IDENTIFIER);
|
||||
assertThrows(IllegalStateException.class, () -> service.createState(TEST_REQUEST_IDENTIFIER));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOidcNotEnabledValidateState() {
|
||||
final OidcService service = getServiceWithNoOidcSupport();
|
||||
assertThrows(IllegalStateException.class, () -> service.isStateValid(TEST_REQUEST_IDENTIFIER, new State(TEST_STATE)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOidcUnknownState() {
|
||||
final OidcService service = getServiceWithOidcSupport();
|
||||
assertFalse(service.isStateValid(TEST_REQUEST_IDENTIFIER, new State(TEST_STATE)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateState() {
|
||||
final OidcService service = getServiceWithOidcSupport();
|
||||
final State state = service.createState(TEST_REQUEST_IDENTIFIER);
|
||||
assertTrue(service.isStateValid(TEST_REQUEST_IDENTIFIER, state));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateStateExpiration() throws Exception {
|
||||
final OidcService service = getServiceWithOidcSupportAndCustomExpiration(1, TimeUnit.SECONDS);
|
||||
final State state = service.createState(TEST_REQUEST_IDENTIFIER);
|
||||
|
||||
Thread.sleep(3 * 1000);
|
||||
|
||||
assertFalse(service.isStateValid(TEST_REQUEST_IDENTIFIER, state));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStoreJwtMultipleInvocation() {
|
||||
final OidcService service = getServiceWithOidcSupport();
|
||||
|
||||
final String TEST_JWT1 = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik5pRmkgT0" +
|
||||
"lEQyBVbml0IFRlc3RlciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MzM5MDIyLCJpc3MiOiJuaWZpX3VuaXRfdGVzdF9hd" +
|
||||
"XRoyZyJ9.b4NIl0RONKdVLOH0D1eObdwAEX8qX-ExqB8KuKSZFLw";
|
||||
|
||||
final String TEST_JWT2 = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5ODc2NTQzMjEwIiwibmFtZSI6Ik5pRm" +
|
||||
"kgT0lEQyBVbml0IFRlc3RlciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MzM5MDIyLCJpc3MiOiJuaWZpX3VuaXRfdGVzdF" +
|
||||
"9hdXRob3JpdHkiLCJhdWQiOiJhbGwiLCJ1c2VybmFtZSI6Im9pZGNfdGVzdCIsImVtYWlsIjoib2lkY190ZXN0QG5pZmk" +
|
||||
"uYXBhY2hlLm9yZyJ9.nlYhplDLXeGAwW62rJ_ZnEaG7nxEB4TbaJNK-_pC4WQ";
|
||||
|
||||
service.storeJwt(TEST_REQUEST_IDENTIFIER, TEST_JWT1);
|
||||
assertThrows(IllegalStateException.class, () -> service.storeJwt(TEST_REQUEST_IDENTIFIER, TEST_JWT2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOidcNotEnabledExchangeCodeForLoginAuthenticationToken() {
|
||||
final OidcService service = getServiceWithNoOidcSupport();
|
||||
assertThrows(IllegalStateException.class, () -> service.exchangeAuthorizationCodeForLoginAuthenticationToken(getAuthorizationGrant()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOidcNotEnabledExchangeCodeForAccessToken() {
|
||||
final OidcService service = getServiceWithNoOidcSupport();
|
||||
assertThrows(IllegalStateException.class, () ->service.exchangeAuthorizationCodeForAccessToken(getAuthorizationGrant()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOidcNotEnabledExchangeCodeForIdToken() {
|
||||
final OidcService service = getServiceWithNoOidcSupport();
|
||||
assertThrows(IllegalStateException.class, () -> service.exchangeAuthorizationCodeForIdToken(getAuthorizationGrant()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOidcNotEnabledGetJwt() {
|
||||
final OidcService service = getServiceWithNoOidcSupport();
|
||||
assertThrows(IllegalStateException.class, () -> service.getJwt(TEST_REQUEST_IDENTIFIER));
|
||||
}
|
||||
|
||||
private OidcService getServiceWithNoOidcSupport() {
|
||||
final OidcIdentityProvider provider = mock(OidcIdentityProvider.class);
|
||||
when(provider.isOidcEnabled()).thenReturn(false);
|
||||
|
||||
final OidcService service = new OidcService(provider);
|
||||
assertFalse(service.isOidcEnabled());
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
private OidcService getServiceWithOidcSupport() {
|
||||
final OidcIdentityProvider provider = mock(OidcIdentityProvider.class);
|
||||
when(provider.isOidcEnabled()).thenReturn(true);
|
||||
|
||||
final OidcService service = new OidcService(provider);
|
||||
assertTrue(service.isOidcEnabled());
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
private OidcService getServiceWithOidcSupportAndCustomExpiration(final int duration, final TimeUnit units) throws Exception {
|
||||
final OidcIdentityProvider provider = mock(OidcIdentityProvider.class);
|
||||
when(provider.isOidcEnabled()).thenReturn(true);
|
||||
when(provider.exchangeAuthorizationCodeforLoginAuthenticationToken(any())).then(invocation -> UUID.randomUUID().toString());
|
||||
|
||||
final OidcService service = new OidcService(provider, duration, units);
|
||||
assertTrue(service.isOidcEnabled());
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
private AuthorizationGrant getAuthorizationGrant() {
|
||||
return new AuthorizationCodeGrant(new AuthorizationCode("code"), URI.create("http://localhost:8080/nifi"));
|
||||
}
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc;
|
||||
|
||||
import com.nimbusds.oauth2.sdk.Scope;
|
||||
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class StandardOidcIdentityProviderTest {
|
||||
|
||||
@Test
|
||||
public void testValidateScopes() throws IllegalAccessException {
|
||||
final String additionalScope_profile = "profile";
|
||||
final String additionalScope_abc = "abc";
|
||||
|
||||
final StandardOidcIdentityProvider provider = createOidcProviderWithAdditionalScopes(additionalScope_profile,
|
||||
additionalScope_abc);
|
||||
Scope scope = provider.getScope();
|
||||
|
||||
// two additional scopes are set, two (openid, email) are hard-coded
|
||||
assertEquals(4, scope.toArray().length);
|
||||
assertTrue(scope.contains("openid"));
|
||||
assertTrue(scope.contains("email"));
|
||||
assertTrue(scope.contains(additionalScope_profile));
|
||||
assertTrue(scope.contains(additionalScope_abc));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoDuplicatedScopes() throws IllegalAccessException {
|
||||
final String additionalScopeDuplicate = "abc";
|
||||
|
||||
final StandardOidcIdentityProvider provider = createOidcProviderWithAdditionalScopes(additionalScopeDuplicate,
|
||||
"def", additionalScopeDuplicate);
|
||||
Scope scope = provider.getScope();
|
||||
|
||||
// three additional scopes are set but one is duplicated and mustn't be returned; note that there is
|
||||
// another one inserted in between the duplicated; two (openid, email) are hard-coded
|
||||
assertEquals(4, scope.toArray().length);
|
||||
}
|
||||
|
||||
private StandardOidcIdentityProvider createOidcProviderWithAdditionalScopes(String... additionalScopes) throws IllegalAccessException {
|
||||
final StandardOidcIdentityProvider provider = mock(StandardOidcIdentityProvider.class);
|
||||
NiFiProperties properties = createNiFiPropertiesMockWithAdditionalScopes(Arrays.asList(additionalScopes));
|
||||
Field propertiesField = FieldUtils.getDeclaredField(StandardOidcIdentityProvider.class, "properties", true);
|
||||
propertiesField.set(provider, properties);
|
||||
|
||||
when(provider.isOidcEnabled()).thenReturn(true);
|
||||
when(provider.getScope()).thenCallRealMethod();
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
private NiFiProperties createNiFiPropertiesMockWithAdditionalScopes(List<String> additionalScopes) {
|
||||
NiFiProperties properties = mock(NiFiProperties.class);
|
||||
when(properties.getOidcAdditionalScopes()).thenReturn(additionalScopes);
|
||||
return properties;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,248 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.client.web;
|
||||
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.Cookie;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class OidcBearerTokenRefreshFilterTest {
|
||||
|
||||
private static final String CLIENT_ID = "client-id";
|
||||
|
||||
private static final String REDIRECT_URI = "http://localhost:8080";
|
||||
|
||||
private static final String AUTHORIZATION_URI = "http://localhost/authorize";
|
||||
|
||||
private static final String TOKEN_URI = "http://localhost/token";
|
||||
|
||||
private static final String SUB_CLAIM = "sub";
|
||||
|
||||
private static final String IDENTITY = "user-identity";
|
||||
|
||||
private static final Duration REFRESH_WINDOW = Duration.ZERO;
|
||||
|
||||
private static final String CURRENT_USER_URI = "/flow/current-user";
|
||||
|
||||
private static final String BEARER_TOKEN = "bearer-token";
|
||||
|
||||
private static final String BEARER_TOKEN_REFRESHED = "bearer-token-refreshed";
|
||||
|
||||
private static final String ACCESS_TOKEN = "access-token";
|
||||
|
||||
private static final String REFRESH_TOKEN = "refresh-token";
|
||||
|
||||
private static final String ID_TOKEN = "id-token";
|
||||
|
||||
private static final String EXP_CLAIM = "exp";
|
||||
|
||||
private static final int INSTANT_OFFSET = 1;
|
||||
|
||||
@Mock
|
||||
BearerTokenProvider bearerTokenProvider;
|
||||
|
||||
@Mock
|
||||
BearerTokenResolver bearerTokenResolver;
|
||||
|
||||
@Mock
|
||||
JwtDecoder jwtDecoder;
|
||||
|
||||
@Mock
|
||||
OAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||
|
||||
@Mock
|
||||
OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenResponseClient;
|
||||
|
||||
@Mock
|
||||
OidcAuthorizedClient authorizedClient;
|
||||
|
||||
MockHttpServletRequest request;
|
||||
|
||||
MockHttpServletResponse response;
|
||||
|
||||
MockFilterChain filterChain;
|
||||
|
||||
OidcBearerTokenRefreshFilter filter;
|
||||
|
||||
@BeforeEach
|
||||
void setFilter() {
|
||||
filter = new OidcBearerTokenRefreshFilter(
|
||||
REFRESH_WINDOW,
|
||||
bearerTokenProvider,
|
||||
bearerTokenResolver,
|
||||
jwtDecoder,
|
||||
authorizedClientRepository,
|
||||
refreshTokenResponseClient
|
||||
);
|
||||
request = new MockHttpServletRequest();
|
||||
response = new MockHttpServletResponse();
|
||||
filterChain = new MockFilterChain();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDoFilterPathNotMatched() throws ServletException, IOException {
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verifyNoInteractions(bearerTokenResolver);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDoFilterBearerTokenNotFound() throws ServletException, IOException {
|
||||
request.setServletPath(CURRENT_USER_URI);
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(bearerTokenResolver).resolve(eq(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDoFilterBearerTokenFoundRefreshNotRequired() throws ServletException, IOException {
|
||||
request.setServletPath(CURRENT_USER_URI);
|
||||
|
||||
when(bearerTokenResolver.resolve(eq(request))).thenReturn(BEARER_TOKEN);
|
||||
final Jwt jwt = getJwt(Instant.MAX);
|
||||
when(jwtDecoder.decode(eq(BEARER_TOKEN))).thenReturn(jwt);
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verifyNoInteractions(authorizedClientRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDoFilterRefreshRequiredClientNotFound() throws ServletException, IOException {
|
||||
request.setServletPath(CURRENT_USER_URI);
|
||||
|
||||
when(bearerTokenResolver.resolve(eq(request))).thenReturn(BEARER_TOKEN);
|
||||
final Jwt jwt = getJwt(Instant.now().minusSeconds(INSTANT_OFFSET));
|
||||
when(jwtDecoder.decode(eq(BEARER_TOKEN))).thenReturn(jwt);
|
||||
|
||||
when(authorizedClientRepository.loadAuthorizedClient(eq(OidcRegistrationProperty.REGISTRATION_ID.getProperty()), any(), eq(request))).thenReturn(null);
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
final Cookie cookie = response.getCookie(ApplicationCookieName.AUTHORIZATION_BEARER.getCookieName());
|
||||
assertNull(cookie);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDoFilterRefreshRequiredRefreshTokenNotFound() throws ServletException, IOException {
|
||||
request.setServletPath(CURRENT_USER_URI);
|
||||
|
||||
when(bearerTokenResolver.resolve(eq(request))).thenReturn(BEARER_TOKEN);
|
||||
final Jwt jwt = getJwt(Instant.now().minusSeconds(INSTANT_OFFSET));
|
||||
when(jwtDecoder.decode(eq(BEARER_TOKEN))).thenReturn(jwt);
|
||||
|
||||
when(authorizedClientRepository.loadAuthorizedClient(eq(OidcRegistrationProperty.REGISTRATION_ID.getProperty()), any(), eq(request))).thenReturn(authorizedClient);
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
final Cookie cookie = response.getCookie(ApplicationCookieName.AUTHORIZATION_BEARER.getCookieName());
|
||||
assertNull(cookie);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDoFilterBearerTokenTokenRefreshed() throws ServletException, IOException {
|
||||
request.setServletPath(CURRENT_USER_URI);
|
||||
|
||||
when(bearerTokenResolver.resolve(eq(request))).thenReturn(BEARER_TOKEN);
|
||||
final Instant expiration = Instant.now().minusSeconds(INSTANT_OFFSET);
|
||||
final Jwt jwt = getJwt(expiration);
|
||||
when(jwtDecoder.decode(eq(BEARER_TOKEN))).thenReturn(jwt);
|
||||
|
||||
final Instant issued = expiration.minus(Duration.ofHours(INSTANT_OFFSET));
|
||||
final OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, ACCESS_TOKEN, issued, expiration);
|
||||
final OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(REFRESH_TOKEN, issued);
|
||||
|
||||
final Map<String, Object> claims = Collections.singletonMap(SUB_CLAIM, IDENTITY);
|
||||
final OidcIdToken idToken = new OidcIdToken(ID_TOKEN, issued, expiration, claims);
|
||||
|
||||
when(authorizedClient.getAccessToken()).thenReturn(accessToken);
|
||||
when(authorizedClient.getRefreshToken()).thenReturn(refreshToken);
|
||||
when(authorizedClient.getIdToken()).thenReturn(idToken);
|
||||
when(authorizedClient.getClientRegistration()).thenReturn(getClientRegistration());
|
||||
when(authorizedClient.getPrincipalName()).thenReturn(IDENTITY);
|
||||
when(authorizedClientRepository.loadAuthorizedClient(eq(OidcRegistrationProperty.REGISTRATION_ID.getProperty()), any(), eq(request))).thenReturn(authorizedClient);
|
||||
|
||||
final OAuth2AccessTokenResponse tokenResponse = OAuth2AccessTokenResponse.withToken(ACCESS_TOKEN)
|
||||
.tokenType(OAuth2AccessToken.TokenType.BEARER)
|
||||
.expiresIn(INSTANT_OFFSET)
|
||||
.build();
|
||||
when(refreshTokenResponseClient.getTokenResponse(any())).thenReturn(tokenResponse);
|
||||
|
||||
when(bearerTokenProvider.getBearerToken(any())).thenReturn(BEARER_TOKEN_REFRESHED);
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
final Cookie cookie = response.getCookie(ApplicationCookieName.AUTHORIZATION_BEARER.getCookieName());
|
||||
assertNotNull(cookie);
|
||||
assertEquals(BEARER_TOKEN_REFRESHED, cookie.getValue());
|
||||
}
|
||||
|
||||
private Jwt getJwt(final Instant expiration) {
|
||||
final Map<String, Object> claims = Collections.singletonMap(EXP_CLAIM, expiration);
|
||||
final Instant issued = expiration.minus(Duration.ofHours(INSTANT_OFFSET));
|
||||
return new Jwt(BEARER_TOKEN, issued, expiration, claims, claims);
|
||||
}
|
||||
|
||||
ClientRegistration getClientRegistration() {
|
||||
return ClientRegistration.withRegistrationId(OidcRegistrationProperty.REGISTRATION_ID.getProperty())
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.clientId(CLIENT_ID)
|
||||
.redirectUri(REDIRECT_URI)
|
||||
.authorizationUri(AUTHORIZATION_URI)
|
||||
.tokenUri(TOKEN_URI)
|
||||
.userNameAttributeName(SUB_CLAIM)
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.client.web;
|
||||
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class StandardAuthorizationRequestRepositoryTest {
|
||||
private static final String AUTHORIZATION_REQUEST_URI = "http://localhost/authorize";
|
||||
|
||||
private static final String CLIENT_ID = "client-id";
|
||||
|
||||
private static final int MAX_AGE_EXPIRED = 0;
|
||||
|
||||
@Captor
|
||||
ArgumentCaptor<String> identifierCaptor;
|
||||
|
||||
@Mock
|
||||
Cache cache;
|
||||
|
||||
MockHttpServletRequest httpServletRequest;
|
||||
|
||||
MockHttpServletResponse httpServletResponse;
|
||||
|
||||
StandardAuthorizationRequestRepository repository;
|
||||
|
||||
@BeforeEach
|
||||
void setRepository() {
|
||||
repository = new StandardAuthorizationRequestRepository(cache);
|
||||
httpServletRequest = new MockHttpServletRequest();
|
||||
httpServletResponse = new MockHttpServletResponse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoadAuthorizationRequestNotFound() {
|
||||
final OAuth2AuthorizationRequest authorizationRequest = repository.loadAuthorizationRequest(httpServletRequest);
|
||||
|
||||
assertNull(authorizationRequest);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoadAuthorizationRequestFound() {
|
||||
final String identifier = UUID.randomUUID().toString();
|
||||
httpServletRequest.setCookies(new Cookie(ApplicationCookieName.OIDC_REQUEST_IDENTIFIER.getCookieName(), identifier));
|
||||
|
||||
final OAuth2AuthorizationRequest authorizationRequest = getAuthorizationRequest();
|
||||
when(cache.get(eq(identifier), eq(OAuth2AuthorizationRequest.class))).thenReturn(authorizationRequest);
|
||||
|
||||
final OAuth2AuthorizationRequest loadedAuthorizationRequest = repository.loadAuthorizationRequest(httpServletRequest);
|
||||
|
||||
assertEquals(authorizationRequest, loadedAuthorizationRequest);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveAuthorizationRequestNotFound() {
|
||||
final OAuth2AuthorizationRequest authorizationRequest = repository.removeAuthorizationRequest(httpServletRequest);
|
||||
|
||||
assertNull(authorizationRequest);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveAuthorizationRequestFound() {
|
||||
final String identifier = UUID.randomUUID().toString();
|
||||
httpServletRequest.setCookies(new Cookie(ApplicationCookieName.OIDC_REQUEST_IDENTIFIER.getCookieName(), identifier));
|
||||
|
||||
final OAuth2AuthorizationRequest authorizationRequest = getAuthorizationRequest();
|
||||
when(cache.get(eq(identifier), eq(OAuth2AuthorizationRequest.class))).thenReturn(authorizationRequest);
|
||||
|
||||
final OAuth2AuthorizationRequest removedAuthorizationRequest = repository.removeAuthorizationRequest(httpServletRequest, httpServletResponse);
|
||||
|
||||
assertEquals(authorizationRequest, removedAuthorizationRequest);
|
||||
|
||||
verify(cache).evict(identifierCaptor.capture());
|
||||
final String evictedIdentifier = identifierCaptor.getValue();
|
||||
assertEquals(identifier, evictedIdentifier);
|
||||
|
||||
final Cookie cookie = httpServletResponse.getCookie(ApplicationCookieName.OIDC_REQUEST_IDENTIFIER.getCookieName());
|
||||
assertNotNull(cookie);
|
||||
assertEquals(MAX_AGE_EXPIRED, cookie.getMaxAge());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveAuthorizationRequest() {
|
||||
final OAuth2AuthorizationRequest authorizationRequest = getAuthorizationRequest();
|
||||
|
||||
repository.saveAuthorizationRequest(authorizationRequest, httpServletRequest, httpServletResponse);
|
||||
|
||||
verify(cache).put(identifierCaptor.capture(), eq(authorizationRequest));
|
||||
final String identifier = identifierCaptor.getValue();
|
||||
assertCookieFound(identifier);
|
||||
}
|
||||
|
||||
private void assertCookieFound(final String identifier) {
|
||||
final Cookie cookie = httpServletResponse.getCookie(ApplicationCookieName.OIDC_REQUEST_IDENTIFIER.getCookieName());
|
||||
assertNotNull(cookie);
|
||||
|
||||
final String cookieValue = cookie.getValue();
|
||||
assertEquals(identifier, cookieValue);
|
||||
}
|
||||
|
||||
private OAuth2AuthorizationRequest getAuthorizationRequest() {
|
||||
return OAuth2AuthorizationRequest.authorizationCode()
|
||||
.authorizationRequestUri(AUTHORIZATION_REQUEST_URI)
|
||||
.authorizationUri(AUTHORIZATION_REQUEST_URI)
|
||||
.clientId(CLIENT_ID)
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,222 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.client.web;
|
||||
|
||||
import org.apache.nifi.components.state.Scope;
|
||||
import org.apache.nifi.components.state.StateManager;
|
||||
import org.apache.nifi.components.state.StateMap;
|
||||
import org.apache.nifi.web.security.oidc.client.web.converter.AuthorizedClientConverter;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class StandardOidcAuthorizedClientRepositoryTest {
|
||||
private static final String REGISTRATION_ID = OidcRegistrationProperty.REGISTRATION_ID.getProperty();
|
||||
|
||||
private static final String IDENTITY = "user-identity";
|
||||
|
||||
private static final String ENCODED_CLIENT = "encoded-client";
|
||||
|
||||
private static final String CLIENT_ID = "client-id";
|
||||
|
||||
private static final String REDIRECT_URI = "http://localhost:8080";
|
||||
|
||||
private static final String AUTHORIZATION_URI = "http://localhost/authorize";
|
||||
|
||||
private static final String TOKEN_URI = "http://localhost/token";
|
||||
|
||||
private static final String TOKEN = "token";
|
||||
|
||||
private static final int EXPIRES_OFFSET = 60;
|
||||
|
||||
private static final Scope SCOPE = Scope.LOCAL;
|
||||
|
||||
@Mock
|
||||
StateManager stateManager;
|
||||
|
||||
@Mock
|
||||
StateMap stateMap;
|
||||
|
||||
@Mock
|
||||
AuthorizedClientConverter authorizedClientConverter;
|
||||
|
||||
@Mock
|
||||
OidcAuthorizedClient authorizedClient;
|
||||
|
||||
@Captor
|
||||
ArgumentCaptor<Map<String, String>> stateMapCaptor;
|
||||
|
||||
MockHttpServletRequest request;
|
||||
|
||||
MockHttpServletResponse response;
|
||||
|
||||
StandardOidcAuthorizedClientRepository repository;
|
||||
|
||||
@BeforeEach
|
||||
void setRepository() {
|
||||
repository = new StandardOidcAuthorizedClientRepository(stateManager, authorizedClientConverter);
|
||||
request = new MockHttpServletRequest();
|
||||
response = new MockHttpServletResponse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoadAuthorizedClientNotFound() throws IOException {
|
||||
final Authentication principal = mock(Authentication.class);
|
||||
when(principal.getName()).thenReturn(IDENTITY);
|
||||
|
||||
when(stateManager.getState(eq(SCOPE))).thenReturn(stateMap);
|
||||
|
||||
final OidcAuthorizedClient authorizedClient = repository.loadAuthorizedClient(REGISTRATION_ID, principal, request);
|
||||
|
||||
assertNull(authorizedClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoadAuthorizedClientFound() throws IOException {
|
||||
final Authentication principal = mock(Authentication.class);
|
||||
when(principal.getName()).thenReturn(IDENTITY);
|
||||
|
||||
when(stateMap.get(eq(IDENTITY))).thenReturn(ENCODED_CLIENT);
|
||||
when(stateManager.getState(eq(SCOPE))).thenReturn(stateMap);
|
||||
when(authorizedClientConverter.getDecoded(eq(ENCODED_CLIENT))).thenReturn(authorizedClient);
|
||||
|
||||
final OidcAuthorizedClient authorizedClientFound = repository.loadAuthorizedClient(REGISTRATION_ID, principal, request);
|
||||
|
||||
assertEquals(authorizedClient, authorizedClientFound);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveAuthorizedClient() throws IOException {
|
||||
final OAuth2AuthenticationToken principal = mock(OAuth2AuthenticationToken.class);
|
||||
final OidcUser oidcUser = mock(OidcUser.class);
|
||||
final OidcIdToken idToken = mock(OidcIdToken.class);
|
||||
final OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class);
|
||||
|
||||
when(principal.getName()).thenReturn(IDENTITY);
|
||||
when(principal.getPrincipal()).thenReturn(oidcUser);
|
||||
when(oidcUser.getIdToken()).thenReturn(idToken);
|
||||
when(authorizedClient.getClientRegistration()).thenReturn(getClientRegistration());
|
||||
when(authorizedClient.getPrincipalName()).thenReturn(IDENTITY);
|
||||
when(authorizedClient.getAccessToken()).thenReturn(accessToken);
|
||||
when(stateManager.getState(eq(SCOPE))).thenReturn(stateMap);
|
||||
when(authorizedClientConverter.getEncoded(isA(OidcAuthorizedClient.class))).thenReturn(ENCODED_CLIENT);
|
||||
|
||||
repository.saveAuthorizedClient(authorizedClient, principal, request, response);
|
||||
|
||||
verify(authorizedClientConverter).getEncoded(isA(OidcAuthorizedClient.class));
|
||||
verify(stateManager).setState(stateMapCaptor.capture(), eq(SCOPE));
|
||||
|
||||
final Map<String, String> updatedStateMap = stateMapCaptor.getValue();
|
||||
final String encodedClient = updatedStateMap.get(IDENTITY);
|
||||
assertEquals(ENCODED_CLIENT, encodedClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveAuthorizedClient() throws IOException {
|
||||
final Authentication principal = mock(Authentication.class);
|
||||
when(principal.getName()).thenReturn(IDENTITY);
|
||||
|
||||
when(stateManager.getState(eq(SCOPE))).thenReturn(stateMap);
|
||||
|
||||
repository.removeAuthorizedClient(REGISTRATION_ID, principal, request, response);
|
||||
|
||||
verify(stateManager).setState(stateMapCaptor.capture(), eq(SCOPE));
|
||||
final Map<String, String> updatedStateMap = stateMapCaptor.getValue();
|
||||
assertTrue(updatedStateMap.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveAuthorizedClientStateManagerException() throws IOException {
|
||||
final Authentication principal = mock(Authentication.class);
|
||||
when(principal.getName()).thenReturn(IDENTITY);
|
||||
|
||||
when(stateManager.getState(eq(SCOPE))).thenThrow(new IOException());
|
||||
|
||||
assertDoesNotThrow(() -> repository.removeAuthorizedClient(REGISTRATION_ID, principal, request, response));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteExpiredEmpty() throws IOException {
|
||||
when(stateManager.getState(eq(SCOPE))).thenReturn(stateMap);
|
||||
|
||||
final List<OidcAuthorizedClient> deletedAuthorizedClients = repository.deleteExpired();
|
||||
|
||||
assertTrue(deletedAuthorizedClients.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteExpired() throws IOException {
|
||||
final Map<String, String> currentStateMap = new LinkedHashMap<>();
|
||||
currentStateMap.put(IDENTITY, ENCODED_CLIENT);
|
||||
|
||||
when(stateMap.toMap()).thenReturn(currentStateMap);
|
||||
when(stateManager.getState(eq(SCOPE))).thenReturn(stateMap);
|
||||
|
||||
final Instant issuedAt = Instant.MIN;
|
||||
final Instant expiresAt = Instant.now().minusSeconds(EXPIRES_OFFSET);
|
||||
final OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, TOKEN, issuedAt, expiresAt);
|
||||
when(authorizedClient.getAccessToken()).thenReturn(accessToken);
|
||||
when(authorizedClientConverter.getDecoded(eq(ENCODED_CLIENT))).thenReturn(authorizedClient);
|
||||
|
||||
final List<OidcAuthorizedClient> deletedAuthorizedClients = repository.deleteExpired();
|
||||
|
||||
assertFalse(deletedAuthorizedClients.isEmpty());
|
||||
final OidcAuthorizedClient deletedAuthorizedClient = deletedAuthorizedClients.iterator().next();
|
||||
assertEquals(authorizedClient, deletedAuthorizedClient);
|
||||
}
|
||||
|
||||
ClientRegistration getClientRegistration() {
|
||||
return ClientRegistration.withRegistrationId(OidcRegistrationProperty.REGISTRATION_ID.getProperty())
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.clientId(CLIENT_ID)
|
||||
.redirectUri(REDIRECT_URI)
|
||||
.authorizationUri(AUTHORIZATION_URI)
|
||||
.tokenUri(TOKEN_URI)
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.client.web.converter;
|
||||
|
||||
import org.apache.nifi.encrypt.PropertyEncryptor;
|
||||
import org.apache.nifi.web.security.jwt.provider.SupportedClaim;
|
||||
import org.apache.nifi.web.security.logout.LogoutRequest;
|
||||
import org.apache.nifi.web.security.oidc.client.web.OidcAuthorizedClient;
|
||||
import org.apache.nifi.web.security.oidc.client.web.OidcRegistrationProperty;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class StandardAuthorizedClientConverterTest {
|
||||
private static final String CLIENT_ID = "client-id";
|
||||
|
||||
private static final String REDIRECT_URI = "http://localhost:8080";
|
||||
|
||||
private static final String AUTHORIZATION_URI = "http://localhost/authorize";
|
||||
|
||||
private static final String TOKEN_URI = "http://localhost/token";
|
||||
|
||||
private static final String USER_IDENTITY = LogoutRequest.class.getSimpleName();
|
||||
|
||||
private static final String ACCESS_TOKEN = "access";
|
||||
|
||||
private static final String REFRESH_TOKEN = "refresh";
|
||||
|
||||
private static final String ID_TOKEN = "id";
|
||||
|
||||
@Mock
|
||||
ClientRegistrationRepository clientRegistrationRepository;
|
||||
|
||||
StandardAuthorizedClientConverter converter;
|
||||
|
||||
@BeforeEach
|
||||
void setConverter() {
|
||||
converter = new StandardAuthorizedClientConverter(new StringPropertyEncryptor(), clientRegistrationRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetEncoded() {
|
||||
final OidcAuthorizedClient oidcAuthorizedClient = getOidcAuthorizedClient(new OAuth2RefreshToken(REFRESH_TOKEN, Instant.now()));
|
||||
final String encoded = converter.getEncoded(oidcAuthorizedClient);
|
||||
|
||||
assertNotNull(encoded);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetDecodedInvalid() {
|
||||
final OidcAuthorizedClient oidcAuthorizedClient = converter.getDecoded(String.class.getName());
|
||||
|
||||
assertNull(oidcAuthorizedClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetEncodedDecoded() {
|
||||
final OidcAuthorizedClient oidcAuthorizedClient = getOidcAuthorizedClient(new OAuth2RefreshToken(REFRESH_TOKEN, Instant.now()));
|
||||
final String encoded = converter.getEncoded(oidcAuthorizedClient);
|
||||
|
||||
assertNotNull(encoded);
|
||||
|
||||
final ClientRegistration clientRegistration = getClientRegistration();
|
||||
when(clientRegistrationRepository.findByRegistrationId(eq(OidcRegistrationProperty.REGISTRATION_ID.getProperty()))).thenReturn(clientRegistration);
|
||||
final OidcAuthorizedClient decoded = converter.getDecoded(encoded);
|
||||
|
||||
assertEquals(decoded.getClientRegistration().getRedirectUri(), clientRegistration.getRedirectUri());
|
||||
assertAuthorizedClientEquals(oidcAuthorizedClient, decoded);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetEncodedDecodedNullRefreshToken() {
|
||||
final OidcAuthorizedClient oidcAuthorizedClient = getOidcAuthorizedClient(null);
|
||||
final String encoded = converter.getEncoded(oidcAuthorizedClient);
|
||||
|
||||
assertNotNull(encoded);
|
||||
|
||||
final ClientRegistration clientRegistration = getClientRegistration();
|
||||
when(clientRegistrationRepository.findByRegistrationId(eq(OidcRegistrationProperty.REGISTRATION_ID.getProperty()))).thenReturn(clientRegistration);
|
||||
final OidcAuthorizedClient decoded = converter.getDecoded(encoded);
|
||||
|
||||
assertEquals(decoded.getClientRegistration().getRedirectUri(), clientRegistration.getRedirectUri());
|
||||
assertAuthorizedClientEquals(oidcAuthorizedClient, decoded);
|
||||
}
|
||||
|
||||
void assertAuthorizedClientEquals(final OidcAuthorizedClient expected, final OidcAuthorizedClient actual) {
|
||||
assertNotNull(actual);
|
||||
assertEquals(expected.getPrincipalName(), actual.getPrincipalName());
|
||||
|
||||
assertEquals(expected.getAccessToken().getTokenValue(), actual.getAccessToken().getTokenValue());
|
||||
assertEquals(expected.getAccessToken().getExpiresAt(), actual.getAccessToken().getExpiresAt());
|
||||
|
||||
final OidcIdToken idToken = actual.getIdToken();
|
||||
assertEquals(expected.getIdToken().getTokenValue(), idToken.getTokenValue());
|
||||
assertEquals(expected.getIdToken().getExpiresAt(), idToken.getExpiresAt());
|
||||
assertEquals(USER_IDENTITY, idToken.getSubject());
|
||||
|
||||
final OAuth2RefreshToken expectedRefreshToken = expected.getRefreshToken();
|
||||
if (expectedRefreshToken == null) {
|
||||
assertNull(actual.getRefreshToken());
|
||||
} else {
|
||||
final OAuth2RefreshToken actualRefreshToken = actual.getRefreshToken();
|
||||
assertNotNull(actualRefreshToken);
|
||||
assertEquals(expectedRefreshToken.getTokenValue(), actualRefreshToken.getTokenValue());
|
||||
assertEquals(expectedRefreshToken.getExpiresAt(), actualRefreshToken.getExpiresAt());
|
||||
}
|
||||
}
|
||||
|
||||
OidcAuthorizedClient getOidcAuthorizedClient(final OAuth2RefreshToken refreshToken) {
|
||||
final Instant issuedAt = Instant.now();
|
||||
final Instant expiresAt = Instant.MAX;
|
||||
|
||||
final ClientRegistration clientRegistration = getClientRegistration();
|
||||
final OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, ACCESS_TOKEN, issuedAt, expiresAt);
|
||||
final Map<String, Object> claims = new LinkedHashMap<>();
|
||||
claims.put(SupportedClaim.ISSUED_AT.getClaim(), issuedAt);
|
||||
claims.put(SupportedClaim.EXPIRATION.getClaim(), expiresAt);
|
||||
final OidcIdToken idToken = new OidcIdToken(ID_TOKEN, issuedAt, expiresAt, claims);
|
||||
return new OidcAuthorizedClient(
|
||||
clientRegistration,
|
||||
USER_IDENTITY,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
idToken
|
||||
);
|
||||
}
|
||||
|
||||
ClientRegistration getClientRegistration() {
|
||||
return ClientRegistration.withRegistrationId(OidcRegistrationProperty.REGISTRATION_ID.getProperty())
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.clientId(CLIENT_ID)
|
||||
.redirectUri(REDIRECT_URI)
|
||||
.authorizationUri(AUTHORIZATION_URI)
|
||||
.tokenUri(TOKEN_URI)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static class StringPropertyEncryptor implements PropertyEncryptor {
|
||||
|
||||
@Override
|
||||
public String encrypt(String property) {
|
||||
return property;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decrypt(String encryptedProperty) {
|
||||
return encryptedProperty;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,261 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.logout;
|
||||
|
||||
import org.apache.nifi.admin.service.IdpUserGroupService;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.security.logout.LogoutRequest;
|
||||
import org.apache.nifi.web.security.logout.LogoutRequestManager;
|
||||
import org.apache.nifi.web.security.oidc.client.web.OidcAuthorizedClient;
|
||||
import org.apache.nifi.web.security.oidc.client.web.OidcRegistrationProperty;
|
||||
import org.apache.nifi.web.security.oidc.revocation.TokenRevocationRequest;
|
||||
import org.apache.nifi.web.security.oidc.revocation.TokenRevocationResponse;
|
||||
import org.apache.nifi.web.security.oidc.revocation.TokenRevocationResponseClient;
|
||||
import org.apache.nifi.web.security.oidc.revocation.TokenTypeHint;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class OidcLogoutSuccessHandlerTest {
|
||||
private static final String REQUEST_IDENTIFIER = UUID.randomUUID().toString();
|
||||
|
||||
private static final String CLIENT_ID = "client-id";
|
||||
|
||||
private static final String REDIRECT_URI = "http://localhost:8080";
|
||||
|
||||
private static final String AUTHORIZATION_URI = "http://localhost/authorize";
|
||||
|
||||
private static final String TOKEN_URI = "http://localhost/token";
|
||||
|
||||
private static final String END_SESSION_URI = "http://localhost/end_session";
|
||||
|
||||
private static final String USER_IDENTITY = LogoutRequest.class.getSimpleName();
|
||||
|
||||
private static final String REQUEST_URI = "/nifi-api";
|
||||
|
||||
private static final int SERVER_PORT = 8080;
|
||||
|
||||
private static final String REDIRECTED_URL = "http://localhost:8080/nifi/logout-complete";
|
||||
|
||||
private static final String ACCESS_TOKEN = "access-token";
|
||||
|
||||
private static final String REFRESH_TOKEN = "refresh-token";
|
||||
|
||||
private static final String ID_TOKEN = "oidc-id-token";
|
||||
|
||||
private static final String END_SESSION_REDIRECT_URL = String.format("%s?id_token_hint=%s&post_logout_redirect_uri=%s", END_SESSION_URI, ID_TOKEN, REDIRECTED_URL);
|
||||
|
||||
@Mock
|
||||
IdpUserGroupService idpUserGroupService;
|
||||
|
||||
@Mock
|
||||
ClientRegistrationRepository clientRegistrationRepository;
|
||||
|
||||
@Mock
|
||||
OAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||
|
||||
@Mock
|
||||
Authentication authentication;
|
||||
|
||||
@Mock
|
||||
OAuth2AccessToken accessToken;
|
||||
|
||||
@Mock
|
||||
OAuth2RefreshToken refreshToken;
|
||||
|
||||
@Mock
|
||||
OidcIdToken idToken;
|
||||
|
||||
@Mock
|
||||
TokenRevocationResponseClient tokenRevocationResponseClient;
|
||||
|
||||
@Captor
|
||||
ArgumentCaptor<TokenRevocationRequest> revocationRequestCaptor;
|
||||
|
||||
MockHttpServletRequest httpServletRequest;
|
||||
|
||||
MockHttpServletResponse httpServletResponse;
|
||||
|
||||
LogoutRequestManager logoutRequestManager;
|
||||
|
||||
OidcLogoutSuccessHandler handler;
|
||||
|
||||
@BeforeEach
|
||||
void setHandler() {
|
||||
logoutRequestManager = new LogoutRequestManager();
|
||||
handler = new OidcLogoutSuccessHandler(
|
||||
logoutRequestManager,
|
||||
idpUserGroupService,
|
||||
clientRegistrationRepository,
|
||||
authorizedClientRepository,
|
||||
tokenRevocationResponseClient
|
||||
);
|
||||
httpServletRequest = new MockHttpServletRequest();
|
||||
httpServletRequest.setServerPort(SERVER_PORT);
|
||||
httpServletResponse = new MockHttpServletResponse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOnLogoutSuccessRequestNotFound() throws IOException {
|
||||
setRequestCookie();
|
||||
|
||||
handler.onLogoutSuccess(httpServletRequest, httpServletResponse, authentication);
|
||||
|
||||
final String redirectedUrl = httpServletResponse.getRedirectedUrl();
|
||||
|
||||
assertEquals(REDIRECTED_URL, redirectedUrl);
|
||||
|
||||
verifyNoInteractions(idpUserGroupService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOnLogoutSuccessRequestFoundEndSessionNotSupported() throws IOException {
|
||||
setRequestCookie();
|
||||
startLogoutRequest();
|
||||
|
||||
final ClientRegistration clientRegistration = getClientRegistrationBuilder().build();
|
||||
when(clientRegistrationRepository.findByRegistrationId(eq(OidcRegistrationProperty.REGISTRATION_ID.getProperty()))).thenReturn(clientRegistration);
|
||||
|
||||
handler.onLogoutSuccess(httpServletRequest, httpServletResponse, authentication);
|
||||
|
||||
final String redirectedUrl = httpServletResponse.getRedirectedUrl();
|
||||
|
||||
assertEquals(REDIRECTED_URL, redirectedUrl);
|
||||
assertUserGroupAuthorizedClientRemoved();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOnLogoutSuccessRequestFoundEndSessionSupportedTokenNotFound() throws IOException {
|
||||
setRequestCookie();
|
||||
startLogoutRequest();
|
||||
|
||||
final Map<String, Object> configurationMetadata = Collections.singletonMap(OidcLogoutSuccessHandler.END_SESSION_ENDPOINT, END_SESSION_URI);
|
||||
final ClientRegistration clientRegistration = getClientRegistrationBuilder().providerConfigurationMetadata(configurationMetadata).build();
|
||||
when(clientRegistrationRepository.findByRegistrationId(eq(OidcRegistrationProperty.REGISTRATION_ID.getProperty()))).thenReturn(clientRegistration);
|
||||
|
||||
handler.onLogoutSuccess(httpServletRequest, httpServletResponse, authentication);
|
||||
|
||||
final String redirectedUrl = httpServletResponse.getRedirectedUrl();
|
||||
|
||||
assertEquals(REDIRECTED_URL, redirectedUrl);
|
||||
assertUserGroupAuthorizedClientRemoved();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOnLogoutSuccessRequestFoundEndSessionSupportedTokenFound() throws IOException {
|
||||
setRequestCookie();
|
||||
startLogoutRequest();
|
||||
|
||||
final Map<String, Object> configurationMetadata = Collections.singletonMap(OidcLogoutSuccessHandler.END_SESSION_ENDPOINT, END_SESSION_URI);
|
||||
final ClientRegistration clientRegistration = getClientRegistrationBuilder().providerConfigurationMetadata(configurationMetadata).build();
|
||||
when(clientRegistrationRepository.findByRegistrationId(eq(OidcRegistrationProperty.REGISTRATION_ID.getProperty()))).thenReturn(clientRegistration);
|
||||
|
||||
when(idToken.getTokenValue()).thenReturn(ID_TOKEN);
|
||||
final OidcAuthorizedClient oidcAuthorizedClient = new OidcAuthorizedClient(
|
||||
clientRegistration,
|
||||
USER_IDENTITY,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
idToken
|
||||
);
|
||||
when(authorizedClientRepository.loadAuthorizedClient(
|
||||
eq(OidcRegistrationProperty.REGISTRATION_ID.getProperty()),
|
||||
isA(Authentication.class),
|
||||
eq(httpServletRequest))
|
||||
).thenReturn(oidcAuthorizedClient);
|
||||
|
||||
final TokenRevocationResponse revocationResponse = new TokenRevocationResponse(true, HttpStatus.OK.value());
|
||||
when(tokenRevocationResponseClient.getRevocationResponse(any())).thenReturn(revocationResponse);
|
||||
when(accessToken.getTokenValue()).thenReturn(ACCESS_TOKEN);
|
||||
when(refreshToken.getTokenValue()).thenReturn(REFRESH_TOKEN);
|
||||
|
||||
handler.onLogoutSuccess(httpServletRequest, httpServletResponse, authentication);
|
||||
|
||||
final String redirectedUrl = httpServletResponse.getRedirectedUrl();
|
||||
|
||||
assertEquals(END_SESSION_REDIRECT_URL, redirectedUrl);
|
||||
assertUserGroupAuthorizedClientRemoved();
|
||||
verify(authorizedClientRepository).removeAuthorizedClient(eq(OidcRegistrationProperty.REGISTRATION_ID.getProperty()), any(), eq(httpServletRequest), eq(httpServletResponse));
|
||||
verify(tokenRevocationResponseClient, times(2)).getRevocationResponse(revocationRequestCaptor.capture());
|
||||
|
||||
final Iterator<TokenRevocationRequest> revocationRequests = revocationRequestCaptor.getAllValues().iterator();
|
||||
|
||||
final TokenRevocationRequest firstRevocationRequest = revocationRequests.next();
|
||||
assertEquals(TokenTypeHint.REFRESH_TOKEN.getHint(), firstRevocationRequest.getTokenTypeHint());
|
||||
assertEquals(REFRESH_TOKEN, firstRevocationRequest.getToken());
|
||||
|
||||
final TokenRevocationRequest secondRevocationRequest = revocationRequests.next();
|
||||
assertEquals(TokenTypeHint.ACCESS_TOKEN.getHint(), secondRevocationRequest.getTokenTypeHint());
|
||||
assertEquals(ACCESS_TOKEN, secondRevocationRequest.getToken());
|
||||
}
|
||||
|
||||
void assertUserGroupAuthorizedClientRemoved() {
|
||||
verify(idpUserGroupService).deleteUserGroups(eq(USER_IDENTITY));
|
||||
}
|
||||
|
||||
void setRequestCookie() {
|
||||
final Cookie cookie = new Cookie(ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER.getCookieName(), REQUEST_IDENTIFIER);
|
||||
httpServletRequest.setCookies(cookie);
|
||||
httpServletRequest.setRequestURI(REQUEST_URI);
|
||||
}
|
||||
|
||||
void startLogoutRequest() {
|
||||
final LogoutRequest logoutRequest = new LogoutRequest(REQUEST_IDENTIFIER, USER_IDENTITY);
|
||||
logoutRequestManager.start(logoutRequest);
|
||||
}
|
||||
|
||||
ClientRegistration.Builder getClientRegistrationBuilder() {
|
||||
return ClientRegistration.withRegistrationId(OidcRegistrationProperty.REGISTRATION_ID.getProperty())
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.clientId(CLIENT_ID)
|
||||
.redirectUri(REDIRECT_URI)
|
||||
.authorizationUri(AUTHORIZATION_URI)
|
||||
.tokenUri(TOKEN_URI);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.registration;
|
||||
|
||||
import com.nimbusds.oauth2.sdk.Scope;
|
||||
import com.nimbusds.oauth2.sdk.id.Issuer;
|
||||
import com.nimbusds.openid.connect.sdk.SubjectType;
|
||||
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.oidc.OidcConfigurationException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.AuthenticationMethod;
|
||||
import org.springframework.web.client.RestOperations;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class StandardClientRegistrationProviderTest {
|
||||
private static final String DISCOVERY_URL = "http://localhost/.well-known/openid-configuration";
|
||||
|
||||
private static final String ISSUER = "http://localhost";
|
||||
|
||||
private static final URI JWK_SET_URI = URI.create("http://localhost/jwks");
|
||||
|
||||
private static final URI TOKEN_ENDPOINT_URI = URI.create("http://localhost/oauth2/v1/token");
|
||||
|
||||
private static final URI USER_INFO_URI = URI.create("http://localhost/oauth2/v1/userinfo");
|
||||
|
||||
private static final URI AUTHORIZATION_ENDPOINT_URI = URI.create("http://localhost/oauth2/v1/authorize");
|
||||
|
||||
private static final String CLIENT_ID = "client-id";
|
||||
|
||||
private static final String CLIENT_SECRET = "client-secret";
|
||||
|
||||
private static final String USER_NAME_ATTRIBUTE_NAME = "email";
|
||||
|
||||
private static final String INVALID_CONFIGURATION = "{}";
|
||||
|
||||
@Mock
|
||||
RestOperations restOperations;
|
||||
|
||||
@Test
|
||||
void testGetClientRegistration() {
|
||||
final NiFiProperties properties = getProperties();
|
||||
final StandardClientRegistrationProvider provider = new StandardClientRegistrationProvider(properties, restOperations);
|
||||
|
||||
final OIDCProviderMetadata providerMetadata = getProviderMetadata();
|
||||
final String serializedMetadata = providerMetadata.toString();
|
||||
|
||||
when(restOperations.getForObject(eq(DISCOVERY_URL), eq(String.class))).thenReturn(serializedMetadata);
|
||||
|
||||
final ClientRegistration clientRegistration = provider.getClientRegistration();
|
||||
|
||||
assertNotNull(clientRegistration);
|
||||
assertEquals(CLIENT_ID, clientRegistration.getClientId());
|
||||
assertEquals(CLIENT_SECRET, clientRegistration.getClientSecret());
|
||||
|
||||
final ClientRegistration.ProviderDetails providerDetails = clientRegistration.getProviderDetails();
|
||||
assertEquals(ISSUER, providerDetails.getIssuerUri());
|
||||
assertEquals(JWK_SET_URI.toString(), providerDetails.getJwkSetUri());
|
||||
assertEquals(AUTHORIZATION_ENDPOINT_URI.toString(), providerDetails.getAuthorizationUri());
|
||||
assertEquals(TOKEN_ENDPOINT_URI.toString(), providerDetails.getTokenUri());
|
||||
|
||||
final ClientRegistration.ProviderDetails.UserInfoEndpoint userInfoEndpoint = providerDetails.getUserInfoEndpoint();
|
||||
assertEquals(USER_INFO_URI.toString(), userInfoEndpoint.getUri());
|
||||
assertEquals(USER_NAME_ATTRIBUTE_NAME, userInfoEndpoint.getUserNameAttributeName());
|
||||
assertEquals(AuthenticationMethod.HEADER, userInfoEndpoint.getAuthenticationMethod());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetClientRegistrationRetrievalFailed() {
|
||||
final NiFiProperties properties = getProperties();
|
||||
final StandardClientRegistrationProvider provider = new StandardClientRegistrationProvider(properties, restOperations);
|
||||
|
||||
when(restOperations.getForObject(eq(DISCOVERY_URL), eq(String.class))).thenThrow(new RuntimeException());
|
||||
|
||||
assertThrows(OidcConfigurationException.class, provider::getClientRegistration);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetClientRegistrationParsingFailed() {
|
||||
final NiFiProperties properties = getProperties();
|
||||
final StandardClientRegistrationProvider provider = new StandardClientRegistrationProvider(properties, restOperations);
|
||||
|
||||
when(restOperations.getForObject(eq(DISCOVERY_URL), eq(String.class))).thenReturn(INVALID_CONFIGURATION);
|
||||
|
||||
assertThrows(OidcConfigurationException.class, provider::getClientRegistration);
|
||||
}
|
||||
|
||||
private NiFiProperties getProperties() {
|
||||
final Properties properties = new Properties();
|
||||
properties.put(NiFiProperties.SECURITY_USER_OIDC_DISCOVERY_URL, DISCOVERY_URL);
|
||||
properties.put(NiFiProperties.SECURITY_USER_OIDC_CLIENT_ID, CLIENT_ID);
|
||||
properties.put(NiFiProperties.SECURITY_USER_OIDC_CLIENT_SECRET, CLIENT_SECRET);
|
||||
properties.put(NiFiProperties.SECURITY_USER_OIDC_CLAIM_IDENTIFYING_USER, USER_NAME_ATTRIBUTE_NAME);
|
||||
return NiFiProperties.createBasicNiFiProperties(null, properties);
|
||||
}
|
||||
|
||||
private OIDCProviderMetadata getProviderMetadata() {
|
||||
final Issuer issuer = new Issuer(ISSUER);
|
||||
final List<SubjectType> subjectTypes = Collections.singletonList(SubjectType.PUBLIC);
|
||||
final OIDCProviderMetadata providerMetadata = new OIDCProviderMetadata(issuer, subjectTypes, JWK_SET_URI);
|
||||
providerMetadata.setTokenEndpointURI(TOKEN_ENDPOINT_URI);
|
||||
providerMetadata.setUserInfoEndpointURI(USER_INFO_URI);
|
||||
providerMetadata.setAuthorizationEndpointURI(AUTHORIZATION_ENDPOINT_URI);
|
||||
final Scope scopes = new Scope();
|
||||
providerMetadata.setScopes(scopes);
|
||||
return providerMetadata;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.revocation;
|
||||
|
||||
import org.apache.nifi.web.security.oidc.client.web.OidcRegistrationProperty;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.web.client.RestOperations;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class StandardTokenRevocationResponseClientTest {
|
||||
private static final String CLIENT_ID = "client-id";
|
||||
|
||||
private static final String CLIENT_SECRET = "client-secret";
|
||||
|
||||
private static final String REDIRECT_URI = "http://localhost:8080";
|
||||
|
||||
private static final String AUTHORIZATION_URI = "http://localhost/authorize";
|
||||
|
||||
private static final String TOKEN_URI = "http://localhost/token";
|
||||
|
||||
private static final String REVOCATION_ENDPOINT_URI = "http://localhost/revoke";
|
||||
|
||||
private static final String TOKEN = "token";
|
||||
|
||||
private static final String TOKEN_TYPE_HINT = "refresh_token";
|
||||
|
||||
@Mock
|
||||
RestOperations restOperations;
|
||||
|
||||
@Mock
|
||||
ClientRegistrationRepository clientRegistrationRepository;
|
||||
|
||||
StandardTokenRevocationResponseClient client;
|
||||
|
||||
@BeforeEach
|
||||
void setClient() {
|
||||
client = new StandardTokenRevocationResponseClient(restOperations, clientRegistrationRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRevocationResponseEndpointNotFound() {
|
||||
final TokenRevocationRequest revocationRequest = new TokenRevocationRequest(TOKEN, TOKEN_TYPE_HINT);
|
||||
|
||||
final ClientRegistration clientRegistration = getClientRegistration(null);
|
||||
when(clientRegistrationRepository.findByRegistrationId(eq(OidcRegistrationProperty.REGISTRATION_ID.getProperty()))).thenReturn(clientRegistration);
|
||||
|
||||
final TokenRevocationResponse revocationResponse = client.getRevocationResponse(revocationRequest);
|
||||
|
||||
assertNotNull(revocationResponse);
|
||||
assertTrue(revocationResponse.isSuccess());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRevocationResponseException() {
|
||||
final TokenRevocationRequest revocationRequest = new TokenRevocationRequest(TOKEN, TOKEN_TYPE_HINT);
|
||||
|
||||
final ClientRegistration clientRegistration = getClientRegistration(REVOCATION_ENDPOINT_URI);
|
||||
when(clientRegistrationRepository.findByRegistrationId(eq(OidcRegistrationProperty.REGISTRATION_ID.getProperty()))).thenReturn(clientRegistration);
|
||||
|
||||
when(restOperations.exchange(any(), eq(String.class))).thenThrow(new RuntimeException());
|
||||
|
||||
final TokenRevocationResponse revocationResponse = client.getRevocationResponse(revocationRequest);
|
||||
|
||||
assertNotNull(revocationResponse);
|
||||
assertFalse(revocationResponse.isSuccess());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRevocationResponse() {
|
||||
final TokenRevocationRequest revocationRequest = new TokenRevocationRequest(TOKEN, TOKEN_TYPE_HINT);
|
||||
|
||||
final ClientRegistration clientRegistration = getClientRegistration(REVOCATION_ENDPOINT_URI);
|
||||
when(clientRegistrationRepository.findByRegistrationId(eq(OidcRegistrationProperty.REGISTRATION_ID.getProperty()))).thenReturn(clientRegistration);
|
||||
|
||||
final ResponseEntity<String> responseEntity = ResponseEntity.ok().build();
|
||||
when(restOperations.exchange(any(), eq(String.class))).thenReturn(responseEntity);
|
||||
|
||||
final TokenRevocationResponse revocationResponse = client.getRevocationResponse(revocationRequest);
|
||||
|
||||
assertNotNull(revocationResponse);
|
||||
assertTrue(revocationResponse.isSuccess());
|
||||
}
|
||||
|
||||
ClientRegistration getClientRegistration(final String revocationEndpoint) {
|
||||
final Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
metadata.put(StandardTokenRevocationResponseClient.REVOCATION_ENDPOINT, revocationEndpoint);
|
||||
return ClientRegistration.withRegistrationId(OidcRegistrationProperty.REGISTRATION_ID.getProperty())
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.clientId(CLIENT_ID)
|
||||
.clientSecret(CLIENT_SECRET)
|
||||
.redirectUri(REDIRECT_URI)
|
||||
.authorizationUri(AUTHORIZATION_URI)
|
||||
.tokenUri(TOKEN_URI)
|
||||
.providerConfigurationMetadata(metadata)
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.nifi.web.security.oidc.web.authentication;
|
||||
|
||||
import org.apache.nifi.admin.service.IdpUserGroupService;
|
||||
import org.apache.nifi.authorization.util.IdentityMapping;
|
||||
import org.apache.nifi.idp.IdpType;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider;
|
||||
import org.apache.nifi.web.security.oidc.client.web.OidcRegistrationProperty;
|
||||
import org.apache.nifi.web.security.oidc.client.web.converter.StandardOAuth2AuthenticationToken;
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||
import org.apache.nifi.web.util.WebUtils;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.Cookie;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class OidcAuthenticationSuccessHandlerTest {
|
||||
|
||||
@Mock
|
||||
OidcUser oidcUser;
|
||||
|
||||
@Mock
|
||||
BearerTokenProvider bearerTokenProvider;
|
||||
|
||||
@Mock
|
||||
IdpUserGroupService idpUserGroupService;
|
||||
|
||||
@Captor
|
||||
ArgumentCaptor<LoginAuthenticationToken> authenticationTokenCaptor;
|
||||
|
||||
MockHttpServletRequest httpServletRequest;
|
||||
|
||||
MockHttpServletResponse httpServletResponse;
|
||||
|
||||
OidcAuthenticationSuccessHandler handler;
|
||||
|
||||
private static final String REQUEST_URI = "/nifi-api";
|
||||
|
||||
private static final String UI_PATH = "/nifi/";
|
||||
|
||||
private static final String ROOT_PATH = "/";
|
||||
|
||||
private static final int SERVER_PORT = 8080;
|
||||
|
||||
private static final String ISSUER = "http://localhost/issuer";
|
||||
|
||||
private static final String LOCALHOST_URL = "http://localhost:8080";
|
||||
|
||||
private static final String TARGET_URL = String.format("%s%s", LOCALHOST_URL, UI_PATH);
|
||||
|
||||
private static final String USER_NAME_CLAIM = "email";
|
||||
|
||||
private static final String GROUPS_CLAIM = "groups";
|
||||
|
||||
private static final String IDENTITY = Authentication.class.getSimpleName();
|
||||
|
||||
private static final String IDENTITY_UPPER = IDENTITY.toUpperCase();
|
||||
|
||||
private static final String AUTHORITY = GrantedAuthority.class.getSimpleName();
|
||||
|
||||
private static final String AUTHORITY_LOWER = AUTHORITY.toLowerCase();
|
||||
|
||||
private static final String ACCESS_TOKEN = "access-token";
|
||||
|
||||
private static final Duration TOKEN_EXPIRATION = Duration.ofHours(1);
|
||||
|
||||
private static final Instant ACCESS_TOKEN_ISSUED = Instant.ofEpochSecond(0);
|
||||
|
||||
private static final Instant ACCESS_TOKEN_EXPIRES = ACCESS_TOKEN_ISSUED.plus(TOKEN_EXPIRATION);
|
||||
|
||||
private static final String FIRST_GROUP = "$1";
|
||||
|
||||
private static final Pattern MATCH_PATTERN = Pattern.compile("(.*)");
|
||||
|
||||
static final String FORWARDED_PATH = "/forwarded";
|
||||
|
||||
static final String FORWARDED_COOKIE_PATH = String.format("%s/", FORWARDED_PATH);
|
||||
|
||||
private static final String FORWARDED_TARGET_URL = String.format("%s%s%s", LOCALHOST_URL, FORWARDED_PATH, UI_PATH);
|
||||
|
||||
private static final String ALLOWED_CONTEXT_PATHS_PARAMETER = "allowedContextPaths";
|
||||
|
||||
private static final IdentityMapping UPPER_IDENTITY_MAPPING = new IdentityMapping(
|
||||
IdentityMapping.Transform.UPPER.toString(),
|
||||
MATCH_PATTERN,
|
||||
FIRST_GROUP,
|
||||
IdentityMapping.Transform.UPPER
|
||||
);
|
||||
|
||||
private static final IdentityMapping LOWER_IDENTITY_MAPPING = new IdentityMapping(
|
||||
IdentityMapping.Transform.LOWER.toString(),
|
||||
MATCH_PATTERN,
|
||||
FIRST_GROUP,
|
||||
IdentityMapping.Transform.LOWER
|
||||
);
|
||||
|
||||
@BeforeEach
|
||||
void setHandler() {
|
||||
handler = new OidcAuthenticationSuccessHandler(
|
||||
bearerTokenProvider,
|
||||
idpUserGroupService,
|
||||
Collections.singletonList(UPPER_IDENTITY_MAPPING),
|
||||
Collections.singletonList(LOWER_IDENTITY_MAPPING),
|
||||
Collections.singletonList(USER_NAME_CLAIM),
|
||||
GROUPS_CLAIM
|
||||
);
|
||||
httpServletRequest = new MockHttpServletRequest();
|
||||
httpServletRequest.setServerPort(SERVER_PORT);
|
||||
httpServletResponse = new MockHttpServletResponse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDetermineTargetUrl() {
|
||||
httpServletRequest.setRequestURI(REQUEST_URI);
|
||||
|
||||
assertTargetUrlEquals(TARGET_URL);
|
||||
assertBearerCookieAdded(ROOT_PATH);
|
||||
assertReplaceUserGroupsInvoked();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDetermineTargetUrlForwardedPath() {
|
||||
final ServletContext servletContext = httpServletRequest.getServletContext();
|
||||
servletContext.setInitParameter(ALLOWED_CONTEXT_PATHS_PARAMETER, FORWARDED_PATH);
|
||||
httpServletRequest.addHeader(WebUtils.FORWARDED_PREFIX_HTTP_HEADER, FORWARDED_PATH);
|
||||
|
||||
httpServletRequest.setRequestURI(REQUEST_URI);
|
||||
|
||||
assertTargetUrlEquals(FORWARDED_TARGET_URL);
|
||||
assertBearerCookieAdded(FORWARDED_COOKIE_PATH);
|
||||
assertReplaceUserGroupsInvoked();
|
||||
}
|
||||
|
||||
void assertReplaceUserGroupsInvoked() {
|
||||
verify(idpUserGroupService).replaceUserGroups(eq(IDENTITY_UPPER), eq(IdpType.OIDC), eq(Collections.singleton(AUTHORITY_LOWER)));
|
||||
}
|
||||
|
||||
void assertTargetUrlEquals(final String expectedTargetUrl) {
|
||||
setOidcUser();
|
||||
|
||||
final StandardOAuth2AuthenticationToken authentication = getAuthenticationToken();
|
||||
|
||||
final String targetUrl = handler.determineTargetUrl(httpServletRequest, httpServletResponse, authentication);
|
||||
|
||||
assertEquals(expectedTargetUrl, targetUrl);
|
||||
}
|
||||
|
||||
void assertBearerCookieAdded(final String expectedCookiePath) {
|
||||
final Cookie responseCookie = httpServletResponse.getCookie(ApplicationCookieName.AUTHORIZATION_BEARER.getCookieName());
|
||||
|
||||
assertNotNull(responseCookie);
|
||||
assertEquals(expectedCookiePath, responseCookie.getPath());
|
||||
|
||||
verify(bearerTokenProvider).getBearerToken(authenticationTokenCaptor.capture());
|
||||
|
||||
final LoginAuthenticationToken authenticationToken = authenticationTokenCaptor.getValue();
|
||||
final Instant expiration = Instant.ofEpochMilli(authenticationToken.getExpiration());
|
||||
|
||||
final ChronoUnit truncation = ChronoUnit.MINUTES;
|
||||
final Instant expirationTruncated = expiration.truncatedTo(truncation);
|
||||
final Instant expected = Instant.now().plus(TOKEN_EXPIRATION).truncatedTo(truncation);
|
||||
assertEquals(expected, expirationTruncated);
|
||||
}
|
||||
|
||||
void setOidcUser() {
|
||||
when(oidcUser.getClaimAsString(eq(USER_NAME_CLAIM))).thenReturn(IDENTITY);
|
||||
when(oidcUser.getClaimAsStringList(eq(GROUPS_CLAIM))).thenReturn(Collections.singletonList(AUTHORITY));
|
||||
when(oidcUser.getIssuer()).thenReturn(getIssuer());
|
||||
}
|
||||
|
||||
StandardOAuth2AuthenticationToken getAuthenticationToken() {
|
||||
final OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, ACCESS_TOKEN, ACCESS_TOKEN_ISSUED, ACCESS_TOKEN_EXPIRES);
|
||||
final Collection<? extends GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority(AUTHORITY));
|
||||
return new StandardOAuth2AuthenticationToken(oidcUser, authorities, OidcRegistrationProperty.REGISTRATION_ID.getProperty(), accessToken);
|
||||
}
|
||||
|
||||
URL getIssuer() {
|
||||
try {
|
||||
return new URL(ISSUER);
|
||||
} catch (final MalformedURLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -58,7 +58,7 @@ class Saml2LocalLogoutFilterTest {
|
|||
|
||||
@Test
|
||||
void testDoFilterInternalNotMatched() throws ServletException, IOException {
|
||||
filter.doFilterInternal(httpServletRequest, httpServletResponse, filterChain);
|
||||
filter.doFilter(httpServletRequest, httpServletResponse, filterChain);
|
||||
|
||||
verifyNoInteractions(logoutSuccessHandler);
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ class Saml2LocalLogoutFilterTest {
|
|||
@Test
|
||||
void testDoFilterInternal() throws ServletException, IOException {
|
||||
httpServletRequest.setPathInfo(SamlUrlPath.LOCAL_LOGOUT_REQUEST.getPath());
|
||||
filter.doFilterInternal(httpServletRequest, httpServletResponse, filterChain);
|
||||
filter.doFilter(httpServletRequest, httpServletResponse, filterChain);
|
||||
|
||||
verify(logoutSuccessHandler).onLogoutSuccess(eq(httpServletRequest), eq(httpServletResponse), isNull());
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
|||
import org.apache.nifi.web.security.logout.LogoutRequest;
|
||||
import org.apache.nifi.web.security.logout.LogoutRequestManager;
|
||||
import org.apache.nifi.web.security.saml2.SamlUrlPath;
|
||||
import org.apache.nifi.web.security.token.LogoutAuthenticationToken;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
|
|
@ -34,6 +34,8 @@ import java.net.URI;
|
|||
* Filter for determining appropriate login location.
|
||||
*/
|
||||
public class LoginFilter implements Filter {
|
||||
private static final String OAUTH2_AUTHORIZATION_PATH = "/nifi-api/oauth2/authorization/consumer";
|
||||
|
||||
private static final String SAML2_AUTHENTICATE_FILTER_PATH = "/nifi-api/saml2/authenticate/consumer";
|
||||
|
||||
private ServletContext servletContext;
|
||||
|
@ -49,18 +51,20 @@ public class LoginFilter implements Filter {
|
|||
final boolean supportsKnoxSso = Boolean.parseBoolean(servletContext.getInitParameter("knox-supported"));
|
||||
final boolean supportsSAML = Boolean.parseBoolean(servletContext.getInitParameter("saml-supported"));
|
||||
|
||||
if (supportsOidc) {
|
||||
final ServletContext apiContext = servletContext.getContext("/nifi-api");
|
||||
apiContext.getRequestDispatcher("/access/oidc/request").forward(request, response);
|
||||
} else if (supportsKnoxSso) {
|
||||
final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
||||
final RequestUriBuilder requestUriBuilder = RequestUriBuilder.fromHttpServletRequest(httpServletRequest);
|
||||
|
||||
if (supportsKnoxSso) {
|
||||
final ServletContext apiContext = servletContext.getContext("/nifi-api");
|
||||
apiContext.getRequestDispatcher("/access/knox/request").forward(request, response);
|
||||
} else if (supportsOidc) {
|
||||
final URI redirectUri = requestUriBuilder.path(OAUTH2_AUTHORIZATION_PATH).build();
|
||||
// Redirect to authorization URL defined in Spring Security OAuth2AuthorizationRequestRedirectFilter
|
||||
sendRedirect(response, redirectUri);
|
||||
} else if (supportsSAML) {
|
||||
final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
||||
final URI authenticateUri = RequestUriBuilder.fromHttpServletRequest(httpServletRequest).path(SAML2_AUTHENTICATE_FILTER_PATH).build();
|
||||
final URI redirectUri = requestUriBuilder.path(SAML2_AUTHENTICATE_FILTER_PATH).build();
|
||||
// Redirect to request consumer URL defined in Spring Security OpenSamlAuthenticationRequestResolver.requestMatcher
|
||||
final HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
||||
httpServletResponse.sendRedirect(authenticateUri.toString());
|
||||
sendRedirect(response, redirectUri);
|
||||
} else {
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
@ -69,4 +73,9 @@ public class LoginFilter implements Filter {
|
|||
@Override
|
||||
public void destroy() {
|
||||
}
|
||||
|
||||
private void sendRedirect(final ServletResponse response, final URI redirectUri) throws IOException {
|
||||
final HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
||||
httpServletResponse.sendRedirect(redirectUri.toString());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,12 @@ import java.net.URI;
|
|||
*/
|
||||
public class LogoutFilter implements Filter {
|
||||
|
||||
private static final String OIDC_LOGOUT_URL = "/nifi-api/access/oidc/logout";
|
||||
|
||||
private static final String SAML_LOCAL_LOGOUT_URL = "/nifi-api/access/saml/local-logout/request";
|
||||
|
||||
private static final String SAML_SINGLE_LOGOUT_URL = "/nifi-api/access/saml/single-logout/request";
|
||||
|
||||
private ServletContext servletContext;
|
||||
|
||||
@Override
|
||||
|
@ -57,19 +63,13 @@ public class LogoutFilter implements Filter {
|
|||
// to retrieve information about the user logging out.
|
||||
|
||||
if (supportsOidc) {
|
||||
final ServletContext apiContext = servletContext.getContext("/nifi-api");
|
||||
apiContext.getRequestDispatcher("/access/oidc/logout").forward(request, response);
|
||||
sendRedirect(OIDC_LOGOUT_URL, request, response);
|
||||
} else if (supportsKnoxSso) {
|
||||
final ServletContext apiContext = servletContext.getContext("/nifi-api");
|
||||
apiContext.getRequestDispatcher("/access/knox/logout").forward(request, response);
|
||||
} else if (supportsSaml) {
|
||||
// Redirect to request URL defined in nifi-web-api security filter configuration
|
||||
final String logoutUrl = supportsSamlSingleLogout ? "/nifi-api/access/saml/single-logout/request" : "/nifi-api/access/saml/local-logout/request";
|
||||
|
||||
final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
||||
final URI targetUri = RequestUriBuilder.fromHttpServletRequest(httpServletRequest).path(logoutUrl).build();
|
||||
final HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
||||
httpServletResponse.sendRedirect(targetUri.toString());
|
||||
final String logoutUrl = supportsSamlSingleLogout ? SAML_SINGLE_LOGOUT_URL : SAML_LOCAL_LOGOUT_URL;
|
||||
sendRedirect(logoutUrl, request, response);
|
||||
} else {
|
||||
final ServletContext apiContext = servletContext.getContext("/nifi-api");
|
||||
apiContext.getRequestDispatcher("/access/logout/complete").forward(request, response);
|
||||
|
@ -79,4 +79,11 @@ public class LogoutFilter implements Filter {
|
|||
@Override
|
||||
public void destroy() {
|
||||
}
|
||||
|
||||
private void sendRedirect(final String logoutUrl, final ServletRequest request, final ServletResponse response) throws IOException {
|
||||
final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
||||
final URI targetUri = RequestUriBuilder.fromHttpServletRequest(httpServletRequest).path(logoutUrl).build();
|
||||
final HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
||||
httpServletResponse.sendRedirect(targetUri.toString());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -113,7 +113,6 @@
|
|||
currentUser: '../nifi-api/flow/current-user',
|
||||
controllerBulletins: '../nifi-api/flow/controller/bulletins',
|
||||
kerberos: '../nifi-api/access/kerberos',
|
||||
oidc: '../nifi-api/access/oidc/exchange',
|
||||
revision: '../nifi-api/flow/revision',
|
||||
banners: '../nifi-api/flow/banners'
|
||||
}
|
||||
|
@ -904,27 +903,19 @@
|
|||
successfulAuthentication(jwt);
|
||||
}).fail(function () {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: config.urls.oidc,
|
||||
dataType: 'text'
|
||||
}).done(function (jwt) {
|
||||
successfulAuthentication(jwt)
|
||||
type: 'GET',
|
||||
url: config.urls.accessTokenExpiration,
|
||||
dataType: 'json'
|
||||
}).done(function (accessTokenExpirationEntity) {
|
||||
var accessTokenExpiration = accessTokenExpirationEntity.accessTokenExpiration;
|
||||
// Convert ISO 8601 string to session expiration in seconds
|
||||
var expiration = Date.parse(accessTokenExpiration.expiration);
|
||||
var expirationSeconds = expiration / 1000;
|
||||
var sessionExpiration = Math.round(expirationSeconds);
|
||||
nfAuthorizationStorage.setToken(sessionExpiration);
|
||||
deferred.resolve();
|
||||
}).fail(function () {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: config.urls.accessTokenExpiration,
|
||||
dataType: 'json'
|
||||
}).done(function (accessTokenExpirationEntity) {
|
||||
var accessTokenExpiration = accessTokenExpirationEntity.accessTokenExpiration;
|
||||
// Convert ISO 8601 string to session expiration in seconds
|
||||
var expiration = Date.parse(accessTokenExpiration.expiration);
|
||||
var expirationSeconds = expiration / 1000;
|
||||
var sessionExpiration = Math.round(expirationSeconds);
|
||||
nfAuthorizationStorage.setToken(sessionExpiration);
|
||||
deferred.resolve();
|
||||
}).fail(function () {
|
||||
deferred.reject();
|
||||
});
|
||||
deferred.reject();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue