Skip to content

Commit

Permalink
Fix #532: OIDC: Implement activation using OAuth 2.0, openid scope
Browse files Browse the repository at this point in the history
  • Loading branch information
banterCZ committed Sep 5, 2024
1 parent 9e349b3 commit c40ab0f
Show file tree
Hide file tree
Showing 18 changed files with 1,189 additions and 4 deletions.
8 changes: 8 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>

<!-- For run at Apple M1 architecture -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-resolver-dns-native-macos</artifactId>
<scope>runtime</scope>
<classifier>osx-aarch_64</classifier>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,17 @@ public enum ActivationType {

/**
* Activation via custom credentials.
* @deprecated Use {@link #DIRECT} instead.
*/
@Deprecated
CUSTOM,

/**
* Direct activation, alias for {@link #CUSTOM}.
* The method could be specified, for example {@code OIDC}.
*/
DIRECT,

/**
* Activation via recovery code.
*/
Expand Down
15 changes: 13 additions & 2 deletions powerauth-restful-security-spring/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,21 @@
</exclusions>
</dependency>

<!-- Other Dependencies -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

<!-- Test Dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* PowerAuth integration libraries for RESTful API applications, examples and
* related software components
*
* Copyright (C) 2024 Wultra s.r.o.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.getlime.security.powerauth.rest.api.spring.exception;

import java.io.Serial;

/**
* Exception related to application configuration.
*
* @author Lubos Racansky, [email protected]
*/
public class PowerAuthApplicationConfigurationException extends Exception {

@Serial
private static final long serialVersionUID = 8677977961740746599L;

/**
* No-arg constructor.
*/
public PowerAuthApplicationConfigurationException() {
super();
}

/**
* Constructor with a custom error message.
* @param message Error message.
*/
public PowerAuthApplicationConfigurationException(String message) {
super(message);
}

/**
* Constructor with a cause.
* @param cause Error cause.
*/
public PowerAuthApplicationConfigurationException(Throwable cause) {
super(cause);
}

/**
* Constructor with a message and cause.
* @param message Error message.
* @param cause Error cause.
*/
public PowerAuthApplicationConfigurationException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
import io.getlime.security.powerauth.rest.api.spring.model.UserInfoContext;
import io.getlime.security.powerauth.rest.api.spring.provider.CustomActivationProvider;
import io.getlime.security.powerauth.rest.api.spring.provider.UserInfoProvider;
import io.getlime.security.powerauth.rest.api.spring.service.oidc.OidcActivationContext;
import io.getlime.security.powerauth.rest.api.spring.service.oidc.OidcHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
Expand All @@ -65,9 +67,12 @@
@Slf4j
public class ActivationService {

private static final String METHOD_OIDC = "oidc";

private final PowerAuthClient powerAuthClient;
private final HttpCustomizationService httpCustomizationService;
private final ActivationContextConverter activationContextConverter;
private final OidcHandler oidcHandler;

private PowerAuthApplicationConfiguration applicationConfiguration;
private CustomActivationProvider activationProvider;
Expand All @@ -81,10 +86,16 @@ public class ActivationService {
* @param activationContextConverter Activation context converter.
*/
@Autowired
public ActivationService(PowerAuthClient powerAuthClient, HttpCustomizationService httpCustomizationService, ActivationContextConverter activationContextConverter) {
public ActivationService(
PowerAuthClient powerAuthClient,
HttpCustomizationService httpCustomizationService,
ActivationContextConverter activationContextConverter,
OidcHandler oidcHandler) {

this.powerAuthClient = powerAuthClient;
this.httpCustomizationService = httpCustomizationService;
this.activationContextConverter = activationContextConverter;
this.oidcHandler = oidcHandler;
}

/**
Expand Down Expand Up @@ -134,7 +145,8 @@ public ActivationLayer1Response createActivation(ActivationLayer1Request request
return switch (type) {
// Regular activation which uses "code" identity attribute
case CODE -> processCodeActivation(eciesContext, request);
case CUSTOM -> processCustomActivation(eciesContext, request);
// Direct activation for known specific methods, otherwise fallback to custom activation
case CUSTOM, DIRECT -> processDirectOrCustomActivation(eciesContext, request, type, identity);
case RECOVERY -> processRecoveryCodeActivation(eciesContext, request);
};
} catch (PowerAuthClientException ex) {
Expand Down Expand Up @@ -430,6 +442,69 @@ private ActivationLayer1Response processCustomActivation(final EncryptionContext
response.getNonce(), response.getTimestamp(), processedCustomAttributes, userInfo);
}

private ActivationLayer1Response processDirectOrCustomActivation(final EncryptionContext eciesContext, final ActivationLayer1Request request, final ActivationType type, final Map<String, String> identity) throws PowerAuthActivationException, PowerAuthClientException {
if (type == ActivationType.DIRECT) {
final String method = identity.get("method");
if (METHOD_OIDC.equals(method)) {
return processOidcActivation(eciesContext, request);
} else {
logger.info("Unknown method: {} of direct activation, fallback to custom activation", method);
}
}

return processCustomActivation(eciesContext, request);
}

private ActivationLayer1Response processOidcActivation(final EncryptionContext eciesContext, final ActivationLayer1Request request) throws PowerAuthClientException, PowerAuthActivationException {
logger.debug("Processing direct OIDC activation.");

final Map<String, String> identity = request.getIdentityAttributes();
final OidcActivationContext oAuthActivationContext = OidcActivationContext.builder()
.providerId(identity.get("providerId"))
.code(identity.get("code"))
.nonce(identity.get("nonce"))
.applicationKey(eciesContext.getApplicationKey())
.build();

final String userId = oidcHandler.retrieveUserId(oAuthActivationContext);

final EciesEncryptedRequest activationData = request.getActivationData();
final Map<String, Object> customAttributes = Objects.requireNonNullElse(request.getCustomAttributes(), new HashMap<>());

final CreateActivationRequest createRequest = new CreateActivationRequest();
createRequest.setUserId(userId);
createRequest.setGenerateRecoveryCodes(false);
createRequest.setApplicationKey(eciesContext.getApplicationKey());
createRequest.setEphemeralPublicKey(activationData.getEphemeralPublicKey());
createRequest.setEncryptedData(activationData.getEncryptedData());
createRequest.setMac(activationData.getMac());
createRequest.setNonce(activationData.getNonce());
createRequest.setProtocolVersion(eciesContext.getVersion());
createRequest.setTimestamp(activationData.getTimestamp());

final CreateActivationResponse response = powerAuthClient.createActivation(
createRequest,
httpCustomizationService.getQueryParams(),
httpCustomizationService.getHttpHeaders()
);

final String activationId = response.getActivationId();
final String applicationId = response.getApplicationId();

commitActivation(activationId);

final UserInfoContext userInfoContext = UserInfoContext.builder()
.stage(UserInfoStage.ACTIVATION_PROCESS_CUSTOM)
.userId(userId)
.activationId(activationId)
.applicationId(applicationId)
.build();
final Map<String, Object> userInfo = processUserInfo(userInfoContext);

return prepareEncryptedResponse(response.getEncryptedData(), response.getMac(),
response.getNonce(), response.getTimestamp(), customAttributes, userInfo);
}

private static void checkIdentityAttributesPresent(final Map<String, String> identity) throws PowerAuthActivationException {
if (CollectionUtils.isEmpty(identity)) {
throw new PowerAuthActivationException("Identity attributes are missing for activation.");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* PowerAuth integration libraries for RESTful API applications, examples and
* related software components
*
* Copyright (C) 2024 Wultra s.r.o.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.getlime.security.powerauth.rest.api.spring.service.oidc;

import com.fasterxml.jackson.annotation.JsonProperty;

/**
* OIDC client authentication methods.
*
* @author Lubos Racansky, [email protected]
*/
enum ClientAuthenticationMethod {

@JsonProperty("client_secret_basic")
CLIENT_SECRET_BASIC,

@JsonProperty("client_secret_post")
CLIENT_SECRET_POST

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* PowerAuth integration libraries for RESTful API applications, examples and
* related software components
*
* Copyright (C) 2024 Wultra s.r.o.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.getlime.security.powerauth.rest.api.spring.service.oidc;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.security.oauth2.jwt.Jwt;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

/**
* Additional ID token validations.
*
* @author Lubos Racansky, [email protected]
*/
@Slf4j
final class IdTokenValidator {

private IdTokenValidator() {
throw new IllegalStateException("Should not be instantiated");
}

static boolean isAtHashValid(final Jwt idToken, final String accessToken) {
final String atHash = idToken.getClaimAsString("at_hash");
return atHash == null || isAtHashValid(accessToken, atHash, idToken.getHeaders().get("alg").toString());
}

static boolean isNonceValid(final Jwt idToken, final String nonce) {
return nonce.equals(idToken.getClaimAsString("nonce"));
}

/**
* <ol>
* <li>Hash the octets of the ASCII representation of the access_token with the hash algorithm for the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, the hash algorithm used is SHA-256.</li>
* <li>Take the left-most half of the hash and base64url-encode it.</li>
* <li>The value of at_hash in the ID Token MUST match the value produced in the previous step.</li>
* </ol>
*
* @see <a href="https://openid.net/specs/openid-connect-core-1_0.html#ImplicitTokenValidation">3.2.2.9. Access Token Validation</a>
*/
private static boolean isAtHashValid(final String accessToken, final String atHash, final String signatureAlgorithm) {
try {
final MessageDigest digest = MessageDigest.getInstance(mapHashAlgorithm(signatureAlgorithm));
final byte[] hash = digest.digest(accessToken.getBytes());
final byte[] leftHalf = new byte[hash.length / 2];
System.arraycopy(hash, 0, leftHalf, 0, leftHalf.length);
final String computedAtHash = Base64.getUrlEncoder().withoutPadding().encodeToString(leftHalf);
return atHash.equals(computedAtHash);
} catch (NoSuchAlgorithmException e) {
logger.error("Unable to validate at_hash", e);
return false;
}
}

private static String mapHashAlgorithm(final String signatureAlgorithm) throws NoSuchAlgorithmException {
return switch (signatureAlgorithm) {
case JwsAlgorithms.RS256, JwsAlgorithms.ES256 -> "SHA-256";
case JwsAlgorithms.RS384, JwsAlgorithms.ES384 -> "SHA-384";
case JwsAlgorithms.RS512, JwsAlgorithms.ES512 -> "SHA-512";
default -> throw new NoSuchAlgorithmException("Unsupported signature algorithm: " + signatureAlgorithm);
};
}
}
Loading

0 comments on commit c40ab0f

Please sign in to comment.