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
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,137 @@
/*
* 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;
@Nullable
private final JtiGenerator jtiGenerator;
@Nullable
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;
this.claimsAudienceProvider = claimsAudienceProvider;
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