Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[versions]
managed-nimbus-jose-jwt = "10.9"
managed-jmacaroons = "0.5.0"
managed-jjwt = "0.13.0"
micronaut = "5.0.0"
micronaut-platform = "5.0.0-RC1"
Expand Down Expand Up @@ -42,6 +43,7 @@ micronaut-data = { module = "io.micronaut.data:micronaut-data-bom", version.ref
micronaut-sql = { module = "io.micronaut.sql:micronaut-sql-bom", version.ref = "micronaut-sql" }

managed-nimbus-jose-jwt = { module = "com.nimbusds:nimbus-jose-jwt", version.ref = "managed-nimbus-jose-jwt" }
managed-jmacaroons = { module = "com.github.nitram509:jmacaroons", version.ref = "managed-jmacaroons" }
managed-jjwt-api = { module = "io.jsonwebtoken:jjwt-api", version.ref = "managed-jjwt" }
managed-jjwt-impl = { module = "io.jsonwebtoken:jjwt-impl", version.ref = "managed-jjwt" }
managed-jjwt-jackson = { module = "io.jsonwebtoken:jjwt-jackson", version.ref = "managed-jjwt" }
Expand All @@ -65,4 +67,3 @@ testcontainers-mysql = { module = "org.testcontainers:testcontainers-mysql" }
testcontainers = { module = "org.testcontainers:testcontainers" }
awaitility = { module = 'org.awaitility:awaitility', version.ref = 'awaitility' }
graalvm-native-buildtools = { module = "org.graalvm.buildtools.native:org.graalvm.buildtools.native.gradle.plugin", version.ref = "graalvm-native-buildtools" }

29 changes: 29 additions & 0 deletions security-macaroons/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
plugins {
id("io.micronaut.build.internal.security-module")
}

dependencies {
api(projects.micronautSecurity)
implementation(libs.managed.jmacaroons)

compileOnly(mn.micronaut.http.server)

testAnnotationProcessor(mn.micronaut.inject.java)
testImplementation(mnTest.micronaut.test.junit5)
testImplementation(mn.micronaut.http.client)
testImplementation(mn.micronaut.http.server.netty)
testImplementation(mnReactor.micronaut.reactor)
testImplementation(mnSerde.micronaut.serde.jackson)
testRuntimeOnly(mnLogging.logback.classic)
testRuntimeOnly(mnTest.junit.jupiter.engine)
}

tasks.withType<Test> {
useJUnitPlatform()
}

micronautBuild {
binaryCompatibility {
enabledAfter("5.1.0")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2017-2026 original authors
*
* Licensed 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
*
* https://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 io.micronaut.security.token.macaroons;

import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.token.AbstractTokenAuthenticationFactory;
import io.micronaut.security.token.RolesFinder;
import io.micronaut.security.token.config.TokenConfiguration;
import jakarta.inject.Singleton;

import java.util.Optional;

/**
* Default {@link MacaroonAuthenticationFactory}.
*
* @author Sergio del Amo
* @since 5.1.0
*/
@Singleton
public class DefaultMacaroonAuthenticationFactory extends AbstractTokenAuthenticationFactory<MacaroonAuthenticationContext> implements MacaroonAuthenticationFactory {

/**
* @param tokenConfiguration Token configuration
* @param rolesFinder Roles finder
*/
public DefaultMacaroonAuthenticationFactory(TokenConfiguration tokenConfiguration,
RolesFinder rolesFinder) {
super(tokenConfiguration, rolesFinder);
}

@Override
public Optional<Authentication> createAuthentication(MacaroonAuthenticationContext context) {
return createAuthentication(context.getClaims());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright 2017-2026 original authors
*
* Licensed 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
*
* https://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 io.micronaut.security.token.macaroons;

import io.micronaut.context.annotation.Requires;
import io.micronaut.context.annotation.Secondary;
import io.micronaut.context.env.Environment;
import io.micronaut.core.util.StringUtils;
import io.micronaut.runtime.ApplicationConfiguration;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.token.Claims;
import io.micronaut.security.token.claims.ClaimsAudienceProvider;
import io.micronaut.security.token.claims.ClaimsGenerator;
import io.micronaut.security.token.claims.JtiGenerator;
import io.micronaut.security.token.config.TokenConfiguration;
import io.micronaut.security.token.config.TokenConfigurationProperties;
import jakarta.inject.Singleton;
import org.jspecify.annotations.Nullable;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
* Default claims generator used when no other {@link ClaimsGenerator} is available.
*/
@Requires(property = TokenConfigurationProperties.PREFIX + ".enabled", notEquals = StringUtils.FALSE)
@Requires(property = MacaroonConfigurationProperties.PREFIX + ".enabled", notEquals = StringUtils.FALSE)
@Requires(property = MacaroonConfigurationProperties.PREFIX + ".secret")
@Secondary
@Singleton
class DefaultMacaroonClaimsGenerator implements ClaimsGenerator {

private static final String ROLES_KEY = "rolesKey";

private final TokenConfiguration tokenConfiguration;
private final JtiGenerator jtiGenerator;
private final ClaimsAudienceProvider claimsAudienceProvider;
private final String appName;

DefaultMacaroonClaimsGenerator(TokenConfiguration tokenConfiguration,
@Nullable JtiGenerator jtiGenerator,
@Nullable ClaimsAudienceProvider claimsAudienceProvider,
@Nullable ApplicationConfiguration applicationConfiguration) {
this.tokenConfiguration = tokenConfiguration;
this.jtiGenerator = jtiGenerator;

Check warning on line 63 in security-macaroons/src/main/java/io/micronaut/security/token/macaroons/DefaultMacaroonClaimsGenerator.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

"jtiGenerator" is marked "@NullMarked at package level" but is set to null.

See more on https://sonarcloud.io/project/issues?id=micronaut-projects_micronaut-security&issues=AZ5PbP30uPeVtyPh533y&open=AZ5PbP30uPeVtyPh533y&pullRequest=2200
this.claimsAudienceProvider = claimsAudienceProvider;

Check warning on line 64 in security-macaroons/src/main/java/io/micronaut/security/token/macaroons/DefaultMacaroonClaimsGenerator.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

"claimsAudienceProvider" is marked "@NullMarked at package level" but is set to null.

See more on https://sonarcloud.io/project/issues?id=micronaut-projects_micronaut-security&issues=AZ5PbP30uPeVtyPh533z&open=AZ5PbP30uPeVtyPh533z&pullRequest=2200
this.appName = applicationConfiguration != null ? applicationConfiguration.getName().orElse(Environment.MICRONAUT) : Environment.MICRONAUT;
}
Comment thread
sdelamo marked this conversation as resolved.

@Override
public Map<String, Object> generateClaims(Authentication authentication, @Nullable Integer expiration) {
Map<String, Object> claims = new LinkedHashMap<>();
populateIat(claims);
populateExp(claims, expiration);
populateJti(claims);
populateIss(claims);
populateAud(claims);
populateNbf(claims);
populateWithAuthentication(claims, authentication);
return claims;
}

@Override
public Map<String, Object> generateClaimsSet(Map<String, ?> oldClaims, Integer expiration) {
Map<String, Object> claims = new LinkedHashMap<>();
List<String> excludedClaims = Arrays.asList(Claims.EXPIRATION_TIME, Claims.ISSUED_AT, Claims.NOT_BEFORE);
oldClaims.forEach((key, value) -> {
if (!excludedClaims.contains(key)) {
claims.put(key, value);
}
});
populateExp(claims, expiration);
populateIat(claims);
populateNbf(claims);
return claims;
}

private void populateIss(Map<String, Object> claims) {
claims.put(Claims.ISSUER, appName);
}

private void populateAud(Map<String, Object> claims) {
if (claimsAudienceProvider != null) {
claims.put(Claims.AUDIENCE, claimsAudienceProvider.audience());
}
}

private void populateExp(Map<String, Object> claims, @Nullable Integer expiration) {
if (expiration != null) {
claims.put(Claims.EXPIRATION_TIME, Date.from(Instant.now().plus(expiration, ChronoUnit.SECONDS)));
}
}

private void populateNbf(Map<String, Object> claims) {
claims.put(Claims.NOT_BEFORE, new Date());
}

private void populateIat(Map<String, Object> claims) {
claims.put(Claims.ISSUED_AT, new Date());
}

private void populateJti(Map<String, Object> claims) {
if (jtiGenerator != null) {
claims.put(Claims.TOKEN_ID, jtiGenerator.generateJtiClaim());
}
}

private void populateWithAuthentication(Map<String, Object> claims, Authentication authentication) {
claims.put(Claims.SUBJECT, authentication.getName());
claims.putAll(authentication.getAttributes());
String rolesKey = tokenConfiguration.getRolesName();
if (!rolesKey.equalsIgnoreCase(TokenConfiguration.DEFAULT_ROLES_NAME)) {
claims.put(ROLES_KEY, rolesKey);
}
claims.put(rolesKey, authentication.getRoles());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2017-2026 original authors
*
* Licensed 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
*
* https://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 io.micronaut.security.token.macaroons;

import java.util.List;
import java.util.Map;

/**
* Signature-verified Macaroon data used to build an authentication.
*
* @author Sergio del Amo
* @since 5.1.0
*/
public final class MacaroonAuthenticationContext {

Check warning on line 27 in security-macaroons/src/main/java/io/micronaut/security/token/macaroons/MacaroonAuthenticationContext.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this class declaration to use 'record MacaroonAuthenticationContext(String location, String identifier, MacaroonSer...)'.

See more on https://sonarcloud.io/project/issues?id=micronaut-projects_micronaut-security&issues=AZ5PbP6MuPeVtyPh5330&open=AZ5PbP6MuPeVtyPh5330&pullRequest=2200

private final String location;
private final String identifier;
private final MacaroonSerialization serialization;
private final Map<String, Object> claims;
private final List<MacaroonCaveat> caveats;

/**
* @param location The Macaroon location
* @param identifier The Macaroon identifier
* @param serialization The matched serialization
* @param claims Claims decoded from verified first-party caveats
* @param caveats First-party caveats
*/
public MacaroonAuthenticationContext(String location,
String identifier,
MacaroonSerialization serialization,
Map<String, Object> claims,
List<MacaroonCaveat> caveats) {
this.location = location;
this.identifier = identifier;
this.serialization = serialization;
this.claims = Map.copyOf(claims);
this.caveats = List.copyOf(caveats);
}

/**
* @return The Macaroon location
*/
public String getLocation() {
return location;
}

/**
* @return The Macaroon identifier
*/
public String getIdentifier() {
return identifier;
}

/**
* @return The matched serialization
*/
public MacaroonSerialization getSerialization() {
return serialization;
}

/**
* @return Claims decoded from verified first-party caveats
*/
public Map<String, Object> getClaims() {
return claims;
}

/**
* @return First-party caveats
*/
public List<MacaroonCaveat> getCaveats() {
return caveats;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2017-2026 original authors
*
* Licensed 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
*
* https://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 io.micronaut.security.token.macaroons;

import io.micronaut.security.authentication.Authentication;

import java.util.Optional;

/**
* Creates an authentication from a verified Macaroon.
*
* @author Sergio del Amo
* @since 5.1.0
*/
@FunctionalInterface
public interface MacaroonAuthenticationFactory {

/**
* @param context The verified Macaroon context
* @return An authentication if the verified Macaroon contains sufficient claims
*/
Optional<Authentication> createAuthentication(MacaroonAuthenticationContext context);
}
Loading
Loading