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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,18 @@ private static void addArrayBrackets(TypeName target, StringBuilder nameBuilder,
private static void updateFromClass(TypeName.BuilderBase<?, ?> builder, Class<?> classType) {
Class<?> componentType = classType.isArray() ? classType.getComponentType() : classType;
builder.packageName(componentType.getPackageName());
builder.className(componentType.getSimpleName());
String className = componentType.getSimpleName();
if (className.isBlank()) {
// anonymous inner classes - name must be guessed from the fully qualified class name
className = componentType.getName();
int lastDollar = className.lastIndexOf('$');
if (lastDollar > 0) {
className = className.substring(lastDollar + 1);
} else {
throw new IllegalArgumentException("Anonymous inner classes must have a name: " + className);
}
}
builder.className(className);
builder.array(classType.isArray());

if (classType.isArray()) {
Expand Down
9 changes: 9 additions & 0 deletions declarative/tests/http/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,18 @@
<groupId>io.helidon.webserver</groupId>
<artifactId>helidon-webserver-security</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.security.abac</groupId>
<artifactId>helidon-security-abac-role</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.security</groupId>
<artifactId>helidon-security-annotations</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.security.providers</groupId>
<artifactId>helidon-security-providers-abac</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.helidon.webclient</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import io.helidon.http.Http;
import io.helidon.http.HttpException;
import io.helidon.http.Status;
import io.helidon.security.SecurityContext;
import io.helidon.security.abac.role.RoleValidator;
import io.helidon.service.registry.Service;
import io.helidon.webserver.http.RestServer;

Expand Down Expand Up @@ -204,6 +206,29 @@ private JsonObject response(String name) {
.build();
}

/**
* Set the greeting to use in future messages.
*
* @param greetingMessage the entity
* @return Hello World message
*/
@Http.POST
@Http.Path("/greeting")
@Http.Consumes(MediaTypes.APPLICATION_JSON_VALUE)
@Http.Produces(MediaTypes.APPLICATION_JSON_VALUE)
@RoleValidator.Roles("admin")
JsonObject updateGreetingHandlerWithSecurity(@Http.Entity JsonObject greetingMessage,
SecurityContext securityContext) {
if (!greetingMessage.containsKey("greeting")) {
// mapped by QuickstartErrorHandler
throw new QuickstartException(Status.BAD_REQUEST_400, "No greeting provided");
}
JsonObject response = response(securityContext.userName());
greeting.set(greetingMessage.getString("greeting"));
return response;
}


private String stringResponse(String name) {
return String.format("%s %s!", greeting.get(), name);
}
Expand Down
8 changes: 7 additions & 1 deletion declarative/tests/http/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,18 @@
requires io.helidon.service.registry;
requires jakarta.json;
requires io.helidon.config.yaml;
requires io.helidon.webserver.security;
requires io.helidon.logging.common;
requires io.helidon.metrics.systemmeters;
requires io.helidon.scheduling;
requires io.helidon.webclient.api;
requires io.helidon.faulttolerance;

// Security related dependencies
requires io.helidon.security;
requires io.helidon.webserver.security;
requires io.helidon.security.abac.role;
requires io.helidon.security.integration.common;
requires io.helidon.security.annotations;

exports io.helidon.declarative.tests.http;
}
12 changes: 12 additions & 0 deletions declarative/tests/http/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,15 @@ server:

greet-service.client:
uri: "http://localhost:${test.server.port}"

security:
providers:
- abac:
- http-basic-auth:
users:
- login: "john"
password: "changeit"
roles: ["admin"]
- login: "jack"
password: "changeit"
roles: ["user"]
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public class ServiceRegistryExtension implements Extension {
// this is needed for a workaround; we cannot depend on security to avoid cyclic dependency
private static final ResolvedType SECURITY = ResolvedType.create("io.helidon.security.Security");

/**
* Creates a new {@link ServiceRegistryExtension}.
Expand Down Expand Up @@ -174,6 +176,10 @@ void registryToCdi(@Observes @Priority(Interceptor.Priority.PLATFORM_AFTER + 200
// we do not want to re-insert CDI beans into CDI, obviously
continue;
}
if (service.contracts().contains(SECURITY)) {
// workaround - we must use `SecurityCdiExtension`, as it has public methods with security builder...
continue;
}
addServiceInfo(abd, bm, registry, processedTypes, service);
}

Expand Down
5 changes: 5 additions & 0 deletions microprofile/jwt-auth/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@
<artifactId>helidon-microprofile-testing-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.helidon.logging</groupId>
<artifactId>helidon-logging-jul</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2023 Oracle and/or its affiliates.
* Copyright (c) 2018, 2025 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -15,12 +15,17 @@
*/
package io.helidon.microprofile.jwt.auth;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.List;

import io.helidon.common.Weight;
import io.helidon.common.Weighted;
import io.helidon.common.config.Config;
import io.helidon.common.types.Annotation;
import io.helidon.common.types.Annotations;
import io.helidon.common.types.TypeName;
import io.helidon.common.types.TypedElementInfo;
import io.helidon.metadata.reflection.AnnotationFactory;
import io.helidon.security.providers.common.spi.AnnotationAnalyzer;

import jakarta.annotation.security.RolesAllowed;
Expand All @@ -36,14 +41,19 @@
@Weight(Weighted.DEFAULT_WEIGHT + 100) // Helidon service loader loaded and ordered
public class JwtAuthAnnotationAnalyzer implements AnnotationAnalyzer {
static final String LOGIN_CONFIG_METHOD = "MP-JWT";
static final TypeName LOGIN_CONFIG = TypeName.create(LoginConfig.class);
private static final TypeName ROLES_ALLOWED = TypeName.create(RolesAllowed.class);

private String authenticator = PROVIDER_NAME;
private boolean secureByDefault;

static boolean isMpJwt(LoginConfig config) {
return LOGIN_CONFIG_METHOD.equals(config.authMethod());
static boolean isMpJwt(Annotation config) {
return config.stringValue("authMethod")
.orElse("")
.equals(LOGIN_CONFIG_METHOD);
}

@SuppressWarnings("removal")
@Override
public void init(Config config) {
config.get(PROVIDER_NAME + ".auth-method-mapping")
Expand All @@ -64,51 +74,43 @@ public void init(Config config) {
.orElse(true);
}

// application class analysis
@Override
public AnalyzerResponse analyze(Class<?> maybeAnnotated) {
public AnalyzerResponse analyze(TypeName applicationType, List<Annotation> annotations) {
AnalyzerResponse.Builder builder = AnalyzerResponse.builder();

LoginConfig annotation = maybeAnnotated.getAnnotation(LoginConfig.class);
var loginConfigAnnotation = Annotations.findFirst(LOGIN_CONFIG, annotations);

if (null == annotation) {
if (loginConfigAnnotation.isEmpty() || !isMpJwt(loginConfigAnnotation.get())) {
builder.register(new RegisterMpJwt(false));
return AnalyzerResponse.abstain();
}

if (isMpJwt(annotation)) {
// triggered!
builder.register(new RegisterMpJwt(true));
// triggered!
builder.register(new RegisterMpJwt(true));

// now if there is a RolesAllowed on application class, automatically required, otherwise optional
// bugfix #455
// we may want to only authenticate requests that hit a @RolesAllowed annotated endpoint
Flag atnFlag = (secureByDefault ? Flag.REQUIRED : Flag.OPTIONAL);
// now if there is a RolesAllowed on application class, automatically required, otherwise optional
// bugfix #455
// we may want to only authenticate requests that hit a @RolesAllowed annotated endpoint
Flag atnFlag = (secureByDefault ? Flag.REQUIRED : Flag.OPTIONAL);

if (isRolesAllowed(maybeAnnotated)) {
atnFlag = Flag.REQUIRED;
}

return builder.authenticationResponse(atnFlag)
.authenticator(authenticator)
.build();
if (isRolesAllowed(annotations)) {
atnFlag = Flag.REQUIRED;
}

builder.register(new RegisterMpJwt(false));
return builder.build();
return builder.authenticationResponse(atnFlag)
.authenticator(authenticator)
.build();
}

// resource class analysis
@Override
public AnalyzerResponse analyze(Class<?> maybeAnnotated, AnalyzerResponse previousResponse) {
public AnalyzerResponse analyze(TypeName endpointType, List<Annotation> annotations, AnalyzerResponse previousResponse) {
return AnalyzerResponse.builder(previousResponse)
.build();
}

// resource method analysis
@Override
public AnalyzerResponse analyze(Method maybeAnnotated, AnalyzerResponse previousResponse) {
if (isRolesAllowed(maybeAnnotated)) {
public AnalyzerResponse analyze(TypeName typeName, TypedElementInfo method, AnalyzerResponse previousResponse) {
if (method.hasAnnotation(ROLES_ALLOWED)) {
return AnalyzerResponse.builder(previousResponse)
.authenticationResponse(Flag.REQUIRED)
.build();
Expand All @@ -117,9 +119,40 @@ public AnalyzerResponse analyze(Method maybeAnnotated, AnalyzerResponse previous
.build();
}

private boolean isRolesAllowed(AnnotatedElement maybeAnnotated) {
RolesAllowed ra = maybeAnnotated.getAnnotation(RolesAllowed.class);
return null != ra;
// application class analysis
@Override
public AnalyzerResponse analyze(Class<?> maybeAnnotated) {
return analyze(TypeName.create(maybeAnnotated), AnnotationFactory.create(maybeAnnotated));
}

// resource class analysis
@SuppressWarnings("removal")
@Override
public AnalyzerResponse analyze(Class<?> maybeAnnotated, AnalyzerResponse previousResponse) {
return AnalyzerResponse.builder(previousResponse)
.build();
}

// resource method analysis
@SuppressWarnings("removal")
@Override
public AnalyzerResponse analyze(Method maybeAnnotated, AnalyzerResponse previousResponse) {
return analyze(TypeName.create(maybeAnnotated.getDeclaringClass()),
AnnotationFactory.create(maybeAnnotated),
previousResponse);
}

@Override
public String toString() {
return "JwtAuthAnnotationAnalyzer{"
+ "authenticator='" + authenticator + '\''
+ ", secureByDefault=" + secureByDefault
+ '}';
}

private boolean isRolesAllowed(List<Annotation> annotations) {
return Annotations.findFirst(ROLES_ALLOWED, annotations)
.isPresent();
}

private static final class RegisterMpJwt {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,21 +220,23 @@ public AuthenticationResponse authenticate(ProviderRequest providerRequest) {
}

//Obtains Application level of security
List<LoginConfig> loginConfigs = providerRequest.endpointConfig().securityLevels().get(0)
.filterAnnotations(LoginConfig.class, EndpointConfig.AnnotationScope.CLASS);
List<io.helidon.common.types.Annotation> loginConfigs = providerRequest.endpointConfig()
.securityLevels()
.getFirst()
.filterAnnotations(JwtAuthAnnotationAnalyzer.LOGIN_CONFIG, EndpointConfig.AnnotationScope.CLASS);

try {
return loginConfigs.stream()
.filter(JwtAuthAnnotationAnalyzer::isMpJwt)
.findFirst()
.map(loginConfig -> authenticate(providerRequest, loginConfig))
.map(loginConfig -> doAuthenticate(providerRequest))
.orElseGet(AuthenticationResponse::abstain);
} catch (java.lang.SecurityException e) {
return AuthenticationResponse.failed("Failed to process authentication header", e);
}
}

AuthenticationResponse authenticate(ProviderRequest providerRequest, LoginConfig loginConfig) {
AuthenticationResponse doAuthenticate(ProviderRequest providerRequest) {
Optional<String> maybeToken;
try {
Map<String, List<String>> headers = providerRequest.env().headers();
Expand Down Expand Up @@ -531,6 +533,7 @@ private JwtOutboundTarget toOutboundTarget(OutboundTarget outboundTarget) {
/**
* A custom object to configure specific handling of outbound calls.
*/
@SuppressWarnings("removal")
public static class JwtOutboundTarget {
private final TokenHandler outboundHandler;
private final String jwtKid;
Expand Down Expand Up @@ -611,6 +614,7 @@ private void update(Jwt.Builder builder) {
/**
* Fluent API builder for {@link JwtAuthProvider}.
*/
@SuppressWarnings("removal")
@Configured(description = "MP-JWT Auth configuration is defined by the spec (options prefixed with `mp.jwt.`), "
+ "and we add a few configuration options for the security provider (options prefixed with "
+ "`security.providers.mp-jwt-auth.`)")
Expand Down
1 change: 1 addition & 0 deletions microprofile/jwt-auth/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
requires transitive io.helidon.security.jwt;
requires transitive io.helidon.security.providers.common;
requires transitive io.helidon.security;
requires io.helidon.metadata.reflection;

exports io.helidon.microprofile.jwt.auth;

Expand Down
Loading
Loading