Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions config/accepted-api-changes.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
[
{
"type": "io.micronaut.security.oauth2.endpoint.token.response.DefaultOpenIdAuthenticationMapper",
"member": "Class io.micronaut.security.oauth2.endpoint.token.response.DefaultOpenIdAuthenticationMapper",
"reason": "createAuthenticationResponse now returns a Publisher"
},
{
"type": "io.micronaut.security.oauth2.endpoint.token.response.DefaultOpenIdAuthenticationMapper",
"member": "Method io.micronaut.security.oauth2.endpoint.token.response.DefaultOpenIdAuthenticationMapper.createAuthenticationResponse(java.lang.String,io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse,io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims,io.micronaut.security.oauth2.endpoint.authorization.state.State)",
"reason": "createAuthenticationResponse now returns a Publisher"
},
{
"type": "io.micronaut.security.oauth2.endpoint.token.response.OpenIdAuthenticationMapper",
"member": "Class io.micronaut.security.oauth2.endpoint.token.response.OpenIdAuthenticationMapper",
"reason": "createAuthenticationResponse now returns a Publisher"
},
{
"type": "io.micronaut.security.oauth2.endpoint.token.response.OpenIdAuthenticationMapper",
"member": "Method io.micronaut.security.oauth2.endpoint.token.response.OpenIdAuthenticationMapper.createAuthenticationResponse(java.lang.String,io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse,io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims,io.micronaut.security.oauth2.endpoint.authorization.state.State)",
"reason": "Make this method Reactive similar to the OauthAuthenticationMapper interface. This allows you to make non-blocking API calls in the OIDC authentication mapper."
},
{
"type": "io.micronaut.security.token.propagation.TokenPropagationConfiguration",
"member": "Implemented interface io.micronaut.http.util.OutgointRequestProcessorMatcher",
Expand Down Expand Up @@ -120,9 +140,9 @@
"reason": "Removed deprecated constructor for Micronaut 4"
},
{
"type": "io.micronaut.security.token.jwt.endpoints.KeysController",
"member": "Constructor io.micronaut.security.token.jwt.endpoints.KeysController(java.util.Collection,com.fasterxml.jackson.databind.ObjectMapper)",
"reason": "Removed deprecated constructor for Micronaut 4"
"type": "io.micronaut.security.token.jwt.endpoints.KeysController",
"member": "Constructor io.micronaut.security.token.jwt.endpoints.KeysController(java.util.Collection,com.fasterxml.jackson.databind.ObjectMapper)",
"reason": "Removed deprecated constructor for Micronaut 4"
},
{
"type": "io.micronaut.security.token.jwt.signature.jwks.JwksSignature",
Expand Down Expand Up @@ -302,22 +322,22 @@
{
"type": "io.micronaut.security.authentication.AuthenticationExceptionHandler",
"member": "Field eventPublisher",
"reason": "Remove deprecated code in mayor version"
"reason": "Remove deprecated code in mayor version"
},
{
"type": "io.micronaut.security.authentication.AuthenticationExceptionHandler",
"member": "Constructor io.micronaut.security.authentication.AuthenticationExceptionHandler(io.micronaut.context.event.ApplicationEventPublisher)",
"reason": "Remove deprecated code in mayor version"
"reason": "Remove deprecated code in mayor version"
},
{
"type": "io.micronaut.security.authentication.DefaultAuthorizationExceptionHandler",
"member": "Constructor io.micronaut.security.authentication.DefaultAuthorizationExceptionHandler(io.micronaut.security.config.RedirectConfiguration,io.micronaut.security.errors.PriorToLoginPersistence)",
"reason": "Remove deprecated code in mayor version"
"reason": "Remove deprecated code in mayor version"
},
{
"type": "io.micronaut.security.authentication.DefaultAuthorizationExceptionHandler",
"member": "Constructor io.micronaut.security.authentication.DefaultAuthorizationExceptionHandler()",
"reason": "Remove deprecated code in mayor version"
"reason": "Remove deprecated code in mayor version"
},
{
"type": "io.micronaut.security.authentication.DefaultAuthorizationExceptionHandler",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;

import java.text.ParseException;
import java.util.Optional;
Expand Down Expand Up @@ -101,12 +100,12 @@ public Publisher<AuthenticationResponse> handle(
return Flux.just(new AuthenticationFailed("State validation failed: " + e.getMessage()));
}
return Flux.from(sendRequest(authorizationResponse, clientConfiguration, tokenEndpoint))
.switchMap(response -> createAuthenticationResponse(authorizationResponse.getNonce(),
clientConfiguration,
openIdProviderMetadata,
response,
authenticationMapper,
authorizationResponse.getState()));
.switchMap(response -> Flux.from(createAuthenticationResponse(authorizationResponse.getNonce(),
clientConfiguration,
openIdProviderMetadata,
response,
authenticationMapper,
authorizationResponse.getState())).map(AuthenticationResponse.class::cast));
}

/**
Expand Down Expand Up @@ -164,28 +163,26 @@ private Flux<AuthenticationResponse> createAuthenticationResponse(String nonce,
OpenIdTokenResponse openIdTokenResponse,
@Nullable OpenIdAuthenticationMapper authenticationMapper,
@Nullable State state) {
return Flux.create(emitter -> {
try {
Optional<AuthenticationResponse> authenticationResponse = validateOpenIdTokenResponse(nonce,
clientConfiguration,
openIdProviderMetadata,
openIdTokenResponse,
authenticationMapper,
state);
if (authenticationResponse.isPresent()) {
emitter.next(authenticationResponse.get());
emitter.complete();
} else {
if (LOG.isTraceEnabled()) {
LOG.trace("Token validation failed. Failing authentication");
}
emitter.error(AuthenticationResponse.exception("JWT validation failed"));

try {
Optional<Publisher<AuthenticationResponse>> authenticationResponse = validateOpenIdTokenResponse(nonce,
clientConfiguration,
openIdProviderMetadata,
openIdTokenResponse,
authenticationMapper,
state);
if (authenticationResponse.isPresent()) {
return Flux.from(authenticationResponse.get());
} else {
if (LOG.isTraceEnabled()) {
LOG.trace("Token validation failed. Failing authentication");
}
} catch (ParseException e) {
//Should never happen as validation succeeded
emitter.error(e);
return Flux.error(AuthenticationResponse.exception("JWT validation failed"));
}
}, FluxSink.OverflowStrategy.ERROR);
} catch (ParseException e) {
// Should never happen as validation succeeded
return Flux.error(e);
}
}

/**
Expand All @@ -199,7 +196,7 @@ private Flux<AuthenticationResponse> createAuthenticationResponse(String nonce,
* @return An Authentication response if the open id token could be validated
* @throws ParseException If the payload of the JWT doesn't represent a valid JSON object and a JWT claims set.
*/
private Optional<AuthenticationResponse> validateOpenIdTokenResponse(String nonce,
private Optional<Publisher<AuthenticationResponse>> validateOpenIdTokenResponse(String nonce,
OauthClientConfiguration clientConfiguration,
OpenIdProviderMetadata openIdProviderMetadata,
OpenIdTokenResponse openIdTokenResponse,
Expand All @@ -215,7 +212,9 @@ private Optional<AuthenticationResponse> validateOpenIdTokenResponse(String nonc
}
OpenIdClaims claims = new JWTOpenIdClaims(jwt.get().getJWTClaimsSet());
OpenIdAuthenticationMapper openIdAuthenticationMapper = authenticationMapper != null ? authenticationMapper : defaultAuthenticationMapper;
return Optional.of(openIdAuthenticationMapper.createAuthenticationResponse(clientConfiguration.getName(), openIdTokenResponse, claims, state));

return Optional.of(Flux.from(openIdAuthenticationMapper.createAuthenticationResponse(clientConfiguration.getName(), openIdTokenResponse, claims, state))
.map(AuthenticationResponse.class::cast));
}
return Optional.empty();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdAuthenticationMapper;
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims;
import io.micronaut.security.oauth2.endpoint.token.response.validation.OpenIdTokenResponseValidator;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;

import java.text.ParseException;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;

/**
* An {@link AuthenticationProvider} that delegates to an OpenID provider using the
Expand All @@ -56,11 +56,11 @@ public class OpenIdPasswordAuthenticationProvider implements AuthenticationProvi
private final OpenIdTokenResponseValidator tokenResponseValidator;

/**
* @param clientConfiguration The client configuration
* @param openIdProviderMetadata The provider metadata
* @param tokenEndpointClient The token endpoint client
* @param clientConfiguration The client configuration
* @param openIdProviderMetadata The provider metadata
* @param tokenEndpointClient The token endpoint client
* @param openIdAuthenticationMapper The user details mapper
* @param tokenResponseValidator The token response validator
* @param tokenResponseValidator The token response validator
*/
public OpenIdPasswordAuthenticationProvider(OauthClientConfiguration clientConfiguration,
OpenIdProviderMetadata openIdProviderMetadata,
Expand All @@ -83,21 +83,21 @@ public Publisher<AuthenticationResponse> authenticate(HttpRequest<?> httpRequest

return Flux.from(
tokenEndpointClient.sendRequest(requestContext))
.switchMap(response -> Flux.create(emitter -> {
Optional<JWT> jwt = tokenResponseValidator.validate(clientConfiguration, openIdProviderMetadata, response, null);
if (jwt.isPresent()) {
try {
OpenIdClaims claims = new JWTOpenIdClaims(jwt.get().getJWTClaimsSet());
emitter.next(openIdAuthenticationMapper.createAuthenticationResponse(clientConfiguration.getName(), response, claims, null));
emitter.complete();
} catch (ParseException e) {
//Should never happen as validation succeeded
emitter.error(e);
}
} else {
emitter.error(AuthenticationResponse.exception("JWT validation failed"));
.switchMap(response -> {
Optional<JWT> jwt = tokenResponseValidator.validate(clientConfiguration, openIdProviderMetadata, response, null);
if (jwt.isPresent()) {
try {
OpenIdClaims claims = new JWTOpenIdClaims(jwt.get().getJWTClaimsSet());
return openIdAuthenticationMapper.createAuthenticationResponse(clientConfiguration.getName(), response, claims, null);
} catch (ParseException e) {
// Should never happen as validation succeeded
return Flux.error(e);
}
}, FluxSink.OverflowStrategy.ERROR));
} else {
return Flux.error(AuthenticationResponse.exception("JWT validation failed"));
}
});

}

/**
Expand All @@ -111,9 +111,9 @@ protected SecureEndpoint getTokenEndpoint(OpenIdProviderMetadata openIdProviderM
List<AuthenticationMethod> authenticationMethods = null;
if (authMethodsSupported != null) {
authenticationMethods = authMethodsSupported.stream()
.map(String::toUpperCase)
.map(AuthenticationMethod::valueOf)
.collect(Collectors.toList());
.map(String::toUpperCase)
.map(AuthenticationMethod::valueOf)
.collect(Collectors.toList());
}
return new DefaultSecureEndpoint(openIdProviderMetadata.getTokenEndpoint(), authenticationMethods);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
import io.micronaut.security.oauth2.endpoint.authorization.state.State;
import io.micronaut.security.token.jwt.generator.claims.JwtClaims;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -60,14 +63,14 @@ public DefaultOpenIdAuthenticationMapper(OpenIdAdditionalClaimsConfiguration ope

@NonNull
@Override
public AuthenticationResponse createAuthenticationResponse(String providerName,
OpenIdTokenResponse tokenResponse,
OpenIdClaims openIdClaims,
@Nullable State state) {
public Publisher<AuthenticationResponse> createAuthenticationResponse(String providerName,
OpenIdTokenResponse tokenResponse,
OpenIdClaims openIdClaims,
@Nullable State state) {
Map<String, Object> claims = buildAttributes(providerName, tokenResponse, openIdClaims);
List<String> roles = getRoles(providerName, tokenResponse, openIdClaims);
String username = getUsername(providerName, tokenResponse, openIdClaims);
return AuthenticationResponse.success(username, roles, claims);
return Flux.just(AuthenticationResponse.success(username, roles, claims));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.micronaut.core.annotation.Nullable;
import io.micronaut.security.authentication.AuthenticationResponse;
import io.micronaut.security.oauth2.endpoint.authorization.state.State;
import org.reactivestreams.Publisher;

/**
* Responsible for converting an OpenID token response to
Expand All @@ -42,8 +43,8 @@ public interface OpenIdAuthenticationMapper {
* @return An authentication response
*/
@NonNull
AuthenticationResponse createAuthenticationResponse(String providerName,
OpenIdTokenResponse tokenResponse,
OpenIdClaims openIdClaims,
@Nullable State state);
Publisher<AuthenticationResponse> createAuthenticationResponse(String providerName,
OpenIdTokenResponse tokenResponse,
OpenIdClaims openIdClaims,
@Nullable State state);
}
7 changes: 6 additions & 1 deletion src/main/docs/guide/breaks.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ This section will document breaking changes that may happen during milestone or

== Micronaut Security 4.0 breaking changes

`micronaut.security.intercept-url-map-prepend-pattern-with-context-path` defaults to `true`. Thus intercept url patterns are be prepended with the server context path if it is set.
- `micronaut.security.intercept-url-map-prepend-pattern-with-context-path` defaults to `true`. Thus intercept url patterns are be prepended with the server context path if it is set.


=== Annotation Mappers Removed

Expand All @@ -23,6 +24,10 @@ The mappers for `javax.annotation.security` annotations – `DenyAll`, `PermitAl

|===

=== Reactive OpenIdAuthenticationMapper

The return type of the `OpenIdAuthenticationMapper.createAuthenticationResponse` has changed to return a `Publisher` to be consistent with the `OauthAuthenticationMapper` interface. Because the method now returns a Publisher, blocking operations can be offloaded to another thread pool using the reactive streams implementation of your choice.

== Micronaut Security 3.4 breaking changes

Sensitive endpoints will now respond with an error unless a replacement for the api:security.rules.SensitiveEndpointRule[] is bound.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import io.micronaut.security.oauth2.endpoint.token.response.OpenIdAuthentication
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse
import jakarta.inject.Singleton
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux

@Singleton
@Replaces(DefaultOpenIdAuthenticationMapper.class)
Expand All @@ -24,8 +26,8 @@ class GlobalOpenIdAuthenticationMapper implements OpenIdAuthenticationMapper {

@Override
@NonNull
AuthenticationResponse createAuthenticationResponse(String providerName, OpenIdTokenResponse tokenResponse, OpenIdClaims openIdClaims, @Nullable State state) {
AuthenticationResponse.success("name")
Publisher<AuthenticationResponse> createAuthenticationResponse(String providerName, OpenIdTokenResponse tokenResponse, OpenIdClaims openIdClaims, @Nullable State state) {
return Flux.just(AuthenticationResponse.success("name"));
}
}
//end::clazz[]
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import io.micronaut.security.oauth2.endpoint.token.response.OpenIdClaims
import io.micronaut.security.oauth2.endpoint.token.response.OpenIdTokenResponse
import jakarta.inject.Named
import jakarta.inject.Singleton
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux

@Singleton
@Named("okta") // <1>
Expand All @@ -23,11 +25,11 @@ class OktaAuthenticationMapper implements OpenIdAuthenticationMapper {

@Override
@NonNull
AuthenticationResponse createAuthenticationResponse(String providerName, // <2>
OpenIdTokenResponse tokenResponse, // <3>
OpenIdClaims openIdClaims, // <4>
@Nullable State state) { // <5>
AuthenticationResponse.success("name") // <6>
Publisher<AuthenticationResponse> createAuthenticationResponse(String providerName, // <2>
OpenIdTokenResponse tokenResponse, // <3>
OpenIdClaims openIdClaims, // <4>
@Nullable State state) { // <5>
Flux.just(AuthenticationResponse.success("name")); // <6>
}
}
//end::clazz[]
Loading