blob: b74908555698acacc0cfdc7eb29e062587d6e048 [file] [log] [blame]
/*
* 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.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.id.State;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.apache.nifi.web.security.util.CacheKey;
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 OidcIdentityProvider identityProvider;
private Cache<CacheKey, State> stateLookupForPendingRequests; // identifier from cookie -> state value
private Cache<CacheKey, String> jwtLookupForCompletedRequests; // identifier from cookie -> jwt or identity (and generate jwt on retrieval)
/**
* Creates a new OtpService with an expiration of 1 minute.
*
* @param identityProvider The identity provider
*/
public OidcService(final OidcIdentityProvider identityProvider) {
this(identityProvider, 60, TimeUnit.SECONDS);
}
/**
* Creates a new OtpService.
*
* @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 = CacheBuilder.newBuilder().expireAfterWrite(duration, units).build();
this.jwtLookupForCompletedRequests = CacheBuilder.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 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(generateStateValue());
try {
synchronized (stateLookupForPendingRequests) {
final State cachedState = stateLookupForPendingRequests.get(oidcRequestIdentifierKey, () -> state);
if (!timeConstantEqualityCheck(state.getValue(), cachedState.getValue())) {
throw new IllegalStateException("An existing login request is already in progress.");
}
}
} catch (ExecutionException e) {
throw new IllegalStateException("Unable to store the login request state.");
}
return state;
}
/**
* Generates a value to use as State in the OpenId Connect 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
*/
private String generateStateValue() {
return new BigInteger(130, new SecureRandom()).toString(32);
}
/**
* 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 && timeConstantEqualityCheck(state.getValue(), proposedState.getValue());
}
}
/**
* Exchanges the specified authorization grant for an ID token for the given request identifier.
*
* @param oidcRequestIdentifier request identifier
* @param authorizationGrant authorization grant
* @throws IOException exceptional case for communication error with the OpenId Connect provider
*/
public void exchangeAuthorizationCode(final String oidcRequestIdentifier, final AuthorizationGrant authorizationGrant) throws IOException {
if (!isOidcEnabled()) {
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
}
final CacheKey oidcRequestIdentifierKey = new CacheKey(oidcRequestIdentifier);
final String nifiJwt = retrieveNifiJwt(authorizationGrant);
try {
// cache the jwt for later retrieval
synchronized (jwtLookupForCompletedRequests) {
final String cachedJwt = jwtLookupForCompletedRequests.get(oidcRequestIdentifierKey, () -> nifiJwt);
if (!timeConstantEqualityCheck(nifiJwt, cachedJwt)) {
throw new IllegalStateException("An existing login request is already in progress.");
}
}
} catch (final ExecutionException e) {
throw new IllegalStateException("Unable to store the login authentication token.");
}
}
/**
* Exchange the authorization code to retrieve a NiFi JWT.
*
* @param authorizationGrant authorization grant
* @return NiFi JWT
* @throws IOException exceptional case for communication error with the OpenId Connect provider
*/
public String retrieveNifiJwt(final AuthorizationGrant authorizationGrant) throws IOException {
return identityProvider.exchangeAuthorizationCode(authorizationGrant);
}
/**
* 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;
}
}
/**
* 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
*/
private 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));
}
}