diff --git a/gradle.properties b/gradle.properties index 1dd9538ef7..59aa7dc463 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,7 @@ spockVersion=2.0-groovy-3.0 gebVersion=4.1 seleniumVersion=3.141.59 +jpasetoVersion = 0.7.0 title=Micronaut Security projectDesc=Official Security Solution for Micronaut diff --git a/security-paseto/build.gradle b/security-paseto/build.gradle new file mode 100644 index 0000000000..2d64f74556 --- /dev/null +++ b/security-paseto/build.gradle @@ -0,0 +1,24 @@ +dependencies { + api "io.micronaut:micronaut-http" + api "io.micronaut:micronaut-http-server" + api project(":security") + api "dev.paseto:jpaseto-api:$jpasetoVersion" + + implementation "io.projectreactor:reactor-core" + + testImplementation "io.micronaut:micronaut-http-client" + testAnnotationProcessor "io.micronaut:micronaut-inject-java" + testImplementation "io.micronaut:micronaut-http-server-netty" + testImplementation project(":test-suite-utils") + testImplementation project(":test-suite-utils-security") + + testRuntimeOnly "dev.paseto:jpaseto-impl:$jpasetoVersion" + testRuntimeOnly "dev.paseto:jpaseto-jackson:$jpasetoVersion" + testRuntimeOnly "dev.paseto:jpaseto-bouncy-castle:$jpasetoVersion" +} +apply from: "${rootProject.projectDir}/gradle/testVerbose.gradle" + +test { + testLogging.showStandardStreams = true + testLogging.exceptionFormat = 'full' +} diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/config/PasetoConfiguration.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/config/PasetoConfiguration.java new file mode 100644 index 0000000000..7eaf80856e --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/config/PasetoConfiguration.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017-2020 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.paseto.config; + +import io.micronaut.core.util.Toggleable; + +/** + * Represents configuration of the PASETO token. + * + * @author Utsav Varia + * @since 3.0 + */ +public interface PasetoConfiguration extends Toggleable { + +} diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/config/PasetoConfigurationProperties.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/config/PasetoConfigurationProperties.java new file mode 100644 index 0000000000..ffe7d26e7c --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/config/PasetoConfigurationProperties.java @@ -0,0 +1,58 @@ +/* + * Copyright 2017-2020 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.paseto.config; + +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.util.StringUtils; +import io.micronaut.security.token.config.TokenConfigurationProperties; + +/** + * {@link PasetoConfiguration} implementation. + * + * @author Utsav Varia + * @since 3.0 + */ +@Requires(property = TokenConfigurationProperties.PREFIX + ".enabled", notEquals = StringUtils.FALSE) +@ConfigurationProperties(PasetoConfigurationProperties.PREFIX) +public class PasetoConfigurationProperties implements PasetoConfiguration { + + public static final String PREFIX = TokenConfigurationProperties.PREFIX + ".paseto"; + /** + * The default enable value. + */ + public static final boolean DEFAULT_ENABLED = true; + + private boolean enabled = DEFAULT_ENABLED; + + /** + * + * @return a boolean flag indicating whether PASETO beans should be enabled or not + */ + @Override + public boolean isEnabled() { + return enabled; + } + + /** + * Sets whether Paseto security is enabled. Default value ({@value #DEFAULT_ENABLED}). + * @param enabled True if it is + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/config/package-info.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/config/package-info.java new file mode 100644 index 0000000000..c06ccd7af4 --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/config/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2020 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. + */ +/** + * PASETO configuration. + * + * @author Utsav Varia + * @since 3.0 + */ +package io.micronaut.security.token.paseto.config; diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/PasetoTokenConfiguration.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/PasetoTokenConfiguration.java new file mode 100644 index 0000000000..420bb3231a --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/PasetoTokenConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2020 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.paseto.generator; + +import dev.paseto.jpaseto.Purpose; +import dev.paseto.jpaseto.Version; + +import javax.crypto.SecretKey; + +/** + * @author Utsav Varia + * @since 3.0 + */ +public interface PasetoTokenConfiguration { + + /** + * @return Paseto Version + */ + default Version getVersion() { + return Version.V1; + } + + default Purpose getPurpose() { + return Purpose.LOCAL; + } + + default SecretKey getSecretKey() { + return null; + } + +} diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/PasetoTokenConfigurationProperties.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/PasetoTokenConfigurationProperties.java new file mode 100644 index 0000000000..21f52b2c48 --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/PasetoTokenConfigurationProperties.java @@ -0,0 +1,107 @@ +/* + * Copyright 2017-2020 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.paseto.generator; + +import dev.paseto.jpaseto.Purpose; +import dev.paseto.jpaseto.Version; +import dev.paseto.jpaseto.lang.Keys; +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.security.token.paseto.config.PasetoConfigurationProperties; + +import javax.crypto.SecretKey; +import java.util.Base64; + +/** + * @author Utsav Varia + * @since 3.0 + */ +@ConfigurationProperties(PasetoTokenConfigurationProperties.PREFIX) +public class PasetoTokenConfigurationProperties implements PasetoTokenConfiguration { + + public static final String PREFIX = PasetoConfigurationProperties.PREFIX + "token"; + + /** + * The default token type. Possible values local, public. + */ + public static final Purpose DEFAULT_PURPOSE = Purpose.PUBLIC; + + /** + * The default Paseto version. Possible values 1, 2. + */ + public static final Version DEFAULT_VERSION = Version.V1; + + /** + * The default secret key. + */ + public static final SecretKey DEFAULT_SECRET_KEY = null; + + private Purpose purpose = DEFAULT_PURPOSE; + private Version version = DEFAULT_VERSION; + private SecretKey secretKey = DEFAULT_SECRET_KEY; + + /** + * @return An integer indicating version of paseto + */ + @Override + public Version getVersion() { + return version; + } + + /** + * Sets Paseto version. + * + * @param version Paseto version + */ + public void setVersion(String version) { + this.version = Version.from(version); + } + + /** + * @return return token type + */ + @Override + public Purpose getPurpose() { + return purpose; + } + + /** + * Sets Paseto token type. + * + * @param purpose Paseto token type + */ + public void setPurpose(String purpose) { + this.purpose = Purpose.from(purpose); + } + + /** + * @return return token type + */ + @Override + public SecretKey getSecretKey() { + return secretKey; + } + + /** + * Sets Secret key for Paseto token to encrypt with. + * + * @param secretKey Secret key for encryption + */ + public void setSecretKey(String secretKey) { + byte[] decodedKey = Base64.getDecoder().decode(secretKey); + this.secretKey = Keys.secretKey(decodedKey); + } + +} diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/PasetoTokenGenerator.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/PasetoTokenGenerator.java new file mode 100644 index 0000000000..7f2abe11f3 --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/PasetoTokenGenerator.java @@ -0,0 +1,98 @@ +/* + * Copyright 2017-2020 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.paseto.generator; + +import dev.paseto.jpaseto.PasetoBuilder; +import dev.paseto.jpaseto.Pasetos; +import dev.paseto.jpaseto.Purpose; +import dev.paseto.jpaseto.Version; +import io.micronaut.security.authentication.Authentication; +import io.micronaut.security.token.generator.TokenGenerator; +import io.micronaut.security.token.paseto.generator.claims.ClaimsGenerator; +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.Optional; + +/** + * @author Utsav Varia + * @since 3.0 + */ +@Singleton +public class PasetoTokenGenerator implements TokenGenerator { + + private static final Logger LOG = LoggerFactory.getLogger(PasetoTokenGenerator.class); + + protected final ClaimsGenerator claimsGenerator; + protected final PasetoTokenConfigurationProperties tokenConfigurationProperties; + + public PasetoTokenGenerator(ClaimsGenerator claimsGenerator, PasetoTokenConfigurationProperties tokenConfigurationProperties) { + this.claimsGenerator = claimsGenerator; + this.tokenConfigurationProperties = tokenConfigurationProperties; + } + + /** + * Returns Paseto Token builder based on configuration. + * + * @return Paseto Builder + */ + public PasetoBuilder getPasetoBuilder() { + if (Version.V1.equals(tokenConfigurationProperties.getVersion())) { + if (Purpose.LOCAL.equals(tokenConfigurationProperties.getPurpose())) { + return Pasetos.V1.LOCAL.builder().setSharedSecret(tokenConfigurationProperties.getSecretKey()); + } else { + return Pasetos.V1.PUBLIC.builder(); + } + } else { + if (Purpose.LOCAL.equals(tokenConfigurationProperties.getPurpose())) { + return Pasetos.V2.LOCAL.builder().setSharedSecret(tokenConfigurationProperties.getSecretKey()); + } else { + return Pasetos.V2.PUBLIC.builder(); + } + } + } + + /** + * Generate a Paseto from a map of claims. + * + * @param claims the map of claims + * @return the created Paseto + */ + protected String generate(final Map claims) { + // claims builder + final PasetoBuilder builder = getPasetoBuilder(); + + // add claims + for (final Map.Entry entry : claims.entrySet()) { + builder.claim(entry.getKey(), entry.getValue()); + } + + return builder.compact(); + } + + @Override + public Optional generateToken(Authentication authentication, Integer expiration) { + Map claims = claimsGenerator.generateClaims(authentication, expiration); + return generateToken(claims); + } + + @Override + public Optional generateToken(Map claims) { + return Optional.of(generate(claims)); + } +} diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/claims/ClaimsAudienceProvider.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/claims/ClaimsAudienceProvider.java new file mode 100644 index 0000000000..065fbad4cb --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/claims/ClaimsAudienceProvider.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2020 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.paseto.generator.claims; + +/** + * @author Utsav Varia + * @since 3.0 + */ +public interface ClaimsAudienceProvider { + /** + * @return a List of Paseto recipients identifiers. + */ + String audience(); +} diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/claims/ClaimsGenerator.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/claims/ClaimsGenerator.java new file mode 100644 index 0000000000..242424b040 --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/claims/ClaimsGenerator.java @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2020 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.paseto.generator.claims; + +import io.micronaut.security.authentication.Authentication; + +import java.util.Map; + +/** + * @author Utsav Varia + * @since 3.0 + */ +public interface ClaimsGenerator { + + /** + * + * @param authentication Authenticated user's representation. + * @param expiration JWT token expiration time in seconds + * @return The Claims + */ + Map generateClaims(Authentication authentication, Integer expiration); + +} diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/claims/PasetoClaims.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/claims/PasetoClaims.java new file mode 100644 index 0000000000..d773813c95 --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/claims/PasetoClaims.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2020 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.paseto.generator.claims; + +import io.micronaut.security.token.Claims; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Utsav Varia + * @since 3.0 + */ +public interface PasetoClaims extends Claims { + + String ISSUER = "iss"; + + String SUBJECT = "sub"; + + String EXPIRATION_TIME = "exp"; + + String NOT_BEFORE = "nbf"; + + String ISSUED_AT = "iat"; + + String JWT_ID = "jti"; + + String AUDIENCE = "aud"; + + List ALL_CLAIMS = Arrays.asList(ISSUER, SUBJECT, EXPIRATION_TIME, NOT_BEFORE, ISSUED_AT, JWT_ID, AUDIENCE); + +} diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/claims/PasetoClaimsGenerator.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/claims/PasetoClaimsGenerator.java new file mode 100644 index 0000000000..2b7a756b90 --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/claims/PasetoClaimsGenerator.java @@ -0,0 +1,175 @@ +/* + * Copyright 2017-2020 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.paseto.generator.claims; + +import io.micronaut.context.env.Environment; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.runtime.ApplicationConfiguration; +import io.micronaut.security.authentication.Authentication; +import io.micronaut.security.token.config.TokenConfiguration; +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Map; + +/** + * @author Utsav Varia + * @since 3.0 + */ +@Singleton +public class PasetoClaimsGenerator implements ClaimsGenerator { + + private static final Logger LOG = LoggerFactory.getLogger(PasetoClaimsGenerator.class); + private static final String ROLES_KEY = "rolesKey"; + + private final TokenConfiguration tokenConfiguration; + private final ClaimsAudienceProvider claimsAudienceProvider; + private final PasetoIdGenerator pasetoIdGenerator; + private final String appName; + + /** + * @param tokenConfiguration Token Configuration + * @param claimsAudienceProvider Generator which creates unique JWT ID + * @param pasetoIdGenerator Provider which identifies the recipients that the JWT is intended for. + * @param applicationConfiguration The application configuration + */ + public PasetoClaimsGenerator(TokenConfiguration tokenConfiguration, + @Nullable ClaimsAudienceProvider claimsAudienceProvider, + @Nullable PasetoIdGenerator pasetoIdGenerator, + @Nullable ApplicationConfiguration applicationConfiguration) { + this.tokenConfiguration = tokenConfiguration; + this.claimsAudienceProvider = claimsAudienceProvider; + this.pasetoIdGenerator = pasetoIdGenerator; + this.appName = applicationConfiguration != null ? applicationConfiguration.getName().orElse(Environment.MICRONAUT) : Environment.MICRONAUT; + } + + /** + * Populates sub claim. + * + * @param builder The Paseto Claims Builder + * @param authentication Authenticated user's representation. + */ + protected void populateSub(PasetoClaimsSet.Builder builder, Authentication authentication) { + builder.subject(authentication.getName()); // sub + } + + /** + * Populates iss claim. + * + * @param builder The Claims Builder + */ + protected void populateIss(PasetoClaimsSet.Builder builder) { + if (appName != null) { + builder.issuer(appName); // iss + } + } + + /** + * Populates aud claim. + * + * @param builder The Claims Builder + */ + protected void populateAud(PasetoClaimsSet.Builder builder) { + if (claimsAudienceProvider != null) { + builder.audience(claimsAudienceProvider.audience()); // aud + } + } + + /** + * Populates exp claim. + * + * @param builder The Claims Builder + * @param expiration expiration time in seconds + */ + protected void populateExp(PasetoClaimsSet.Builder builder, @Nullable Integer expiration) { + if (expiration != null) { + LOG.debug("Setting expiration to {}", expiration); + builder.expiration(Instant.now().plus(expiration, ChronoUnit.SECONDS)); // exp + } + } + + /** + * Populates nbf claim. + * + * @param builder The Claims Builder + */ + protected void populateNbf(PasetoClaimsSet.Builder builder) { + builder.notBefore(Instant.now()); // nbf + } + + /** + * Populates iat claim. + * + * @param builder The Claims Builder + */ + protected void populateIat(PasetoClaimsSet.Builder builder) { + builder.issuedAt(Instant.now()); // iat + } + + /** + * Populates jti claim. + * + * @param builder The Claims Builder + */ + protected void populateJti(PasetoClaimsSet.Builder builder) { + if (pasetoIdGenerator != null) { + builder.tokenId(pasetoIdGenerator.generateJtiClaim()); // jti + } + } + + @Override + public Map generateClaims(Authentication authentication, Integer expiration) { + PasetoClaimsSet.Builder builder = new PasetoClaimsSet.Builder(); + + populateIat(builder); + populateExp(builder, expiration); + populateJti(builder); + populateIss(builder); + populateAud(builder); + populateNbf(builder); + populateWithAuthentication(builder, authentication); + + //TODO: Add Support for footer in token + + PasetoClaimsSet claimsSet = builder.build(); + if (LOG.isDebugEnabled()) { + LOG.debug("Generated claim set:"); + LOG.debug("{"); + claimsSet.getClaims().forEach((key, value) -> LOG.debug("\t{} : {}", key, value)); + LOG.debug("}"); + } + return claimsSet.getClaims(); + } + + /** + * Populates Claims with Authentication object. + * + * @param builder the Claims Builder + * @param authentication Authenticated user's representation. + */ + protected void populateWithAuthentication(PasetoClaimsSet.Builder builder, Authentication authentication) { + populateSub(builder, authentication); + authentication.getAttributes().forEach(builder::claim); + String rolesKey = tokenConfiguration.getRolesName(); + if (!rolesKey.equalsIgnoreCase(TokenConfiguration.DEFAULT_ROLES_NAME)) { + builder.claim(ROLES_KEY, rolesKey); + } + builder.claim(rolesKey, authentication.getRoles()); + } +} diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/claims/PasetoClaimsSet.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/claims/PasetoClaimsSet.java new file mode 100644 index 0000000000..a6501badd2 --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/claims/PasetoClaimsSet.java @@ -0,0 +1,166 @@ +/* + * Copyright 2017-2020 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.paseto.generator.claims; + +import dev.paseto.jpaseto.Claims; + +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * @author Utsav Varia + * @since 3.0 + */ +public final class PasetoClaimsSet { + + private final Map claims = new LinkedHashMap<>(); + + private PasetoClaimsSet(Map claims) { + this.claims.putAll(claims); + } + + public Map getClaims() { + return claims; + } + + public Instant getNotBeforeTime() { + Object value = claims.get(Claims.NOT_BEFORE); + + if (value == null) { + return null; + } else if (value instanceof Instant) { + return (Instant) value; + } else if (value instanceof Number) { + return Instant.ofEpochSecond(((Number) value).longValue()); + } else { + return null; + } + } + + /** + * Builder for creating Paseto Claims builder. + */ + public static class Builder { + + /** + * The claims. + */ + private final Map claims = new LinkedHashMap<>(); + + /** + * Gives the claims map. + * + * @return claims map + */ + public PasetoClaimsSet build() { + return new PasetoClaimsSet(claims); + } + + /** + * Sets the issuer(iss) claim. + * + * @param iss The issuer claim. + * @return This builder. + */ + public Builder issuer(String iss) { + claim(Claims.ISSUER, iss); + return this; + } + + /** + * Sets the subject(sub) claim. + * + * @param sub The subject claim. + * @return This builder. + */ + public Builder subject(String sub) { + claim(Claims.SUBJECT, sub); + return this; + } + + /** + * Sets the audience(aud) claim. + * + * @param aud The audience claim. + * @return This builder. + */ + public Builder audience(String aud) { + claim(Claims.AUDIENCE, aud); + return this; + } + + /** + * Sets the expiery(exp) claim. + * + * @param exp The expiry claim. + * @return This builder. + */ + public Builder expiration(Instant exp) { + claim(Claims.EXPIRATION, exp); + return this; + } + + /** + * Sets not before(nbf) claim. + * + * @param nbf not before claim. + * @return This builder. + */ + public Builder notBefore(Instant nbf) { + claim(Claims.NOT_BEFORE, nbf); + return this; + } + + /** + * Sets the issued at(iat) claim. + * + * @param iat The issued at claim. + * @return This builder. + */ + public Builder issuedAt(Instant iat) { + claim(Claims.ISSUED_AT, iat); + return this; + } + + /** + * Sets the token id(jti) claim. + * + * @param jti The token id claim. + * @return This builder. + */ + public Builder tokenId(String jti) { + claim(Claims.TOKEN_ID, jti); + return this; + } + + //TODO: Add Support for footer in token + + /** + * Set the specified claim. + * + * @param name The name of the claim to set. Must not be {@code null} + * @param value The value of the claim to set, {@code null} jf not specified + * @return this builder. + */ + public Builder claim(final String name, final Object value) { + claims.put(name, value); + return this; + } + + } + +} diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/claims/PasetoIdGenerator.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/claims/PasetoIdGenerator.java new file mode 100644 index 0000000000..92bc6bcb4a --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/generator/claims/PasetoIdGenerator.java @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 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.paseto.generator.claims; + +/** + * @author Utsav Varia + * @since 3.0 + */ +public interface PasetoIdGenerator { + /** + * @return a case-sensitive String which is used as a unique identifier for the Paseto. + */ + String generateJtiClaim(); +} diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/package-info.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/package-info.java new file mode 100644 index 0000000000..ce8011c93f --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/package-info.java @@ -0,0 +1,30 @@ +/* + * Copyright 2017-2020 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. + */ +/** + * Contains classes specific to Platform-Agnostic Security Tokens (PASETO) Authentication within Micronaut. + * + * @author Utsav Varia + * @since 3.0 + */ + +@Configuration +@Requires(property = PasetoConfigurationProperties.PREFIX + ".enabled", notEquals = StringUtils.FALSE) +package io.micronaut.security.token.paseto; + +import io.micronaut.context.annotation.Configuration; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.util.StringUtils; +import io.micronaut.security.token.paseto.config.PasetoConfigurationProperties; diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/DefaultPasetoAuthenticationFactory.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/DefaultPasetoAuthenticationFactory.java new file mode 100644 index 0000000000..b94579a7b8 --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/DefaultPasetoAuthenticationFactory.java @@ -0,0 +1,72 @@ +/* + * Copyright 2017-2020 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.paseto.validator; + +import dev.paseto.jpaseto.Claims; +import dev.paseto.jpaseto.Paseto; +import io.micronaut.security.authentication.Authentication; +import io.micronaut.security.token.RolesFinder; +import io.micronaut.security.token.config.TokenConfiguration; +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; + +/** + * @author Utsav Varia + * @since 3.0 + */ +@Singleton +public class DefaultPasetoAuthenticationFactory implements PasetoAuthenticationFactory { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultPasetoAuthenticationFactory.class); + + private final TokenConfiguration tokenConfiguration; + private final RolesFinder rolesFinder; + + public DefaultPasetoAuthenticationFactory(TokenConfiguration tokenConfiguration, RolesFinder rolesFinder) { + this.tokenConfiguration = tokenConfiguration; + this.rolesFinder = rolesFinder; + } + + @Override + public Optional createAuthentication(Paseto token) { + try { + final Claims attributes = token.getClaims(); + if (attributes == null) { + return Optional.empty(); + } + return usernameForClaims(attributes).map(username -> + Authentication.build(username, + rolesFinder.resolveRoles(attributes), + attributes)); + } catch (Exception e) { + if (LOG.isErrorEnabled()) { + LOG.error("Exception while creating authentication", e); + } + } + return Optional.empty(); + } + + private Optional usernameForClaims(Claims claims) { + String username = claims.get(tokenConfiguration.getNameKey(), String.class); + if (username == null) { + return Optional.ofNullable(claims.getSubject()); + } + return Optional.of(username); + } +} diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/PasetoAuthenticationFactory.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/PasetoAuthenticationFactory.java new file mode 100644 index 0000000000..d0176a66c4 --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/PasetoAuthenticationFactory.java @@ -0,0 +1,27 @@ +/* + * Copyright 2017-2020 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.paseto.validator; + +import dev.paseto.jpaseto.Paseto; +import io.micronaut.security.token.TokenAuthenticationFactory; + +/** + * @author Utsav Varia + * @since 3.0 + */ +public interface PasetoAuthenticationFactory extends TokenAuthenticationFactory { +} diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/PasetoTokenValidator.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/PasetoTokenValidator.java new file mode 100644 index 0000000000..9e9a1178dc --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/PasetoTokenValidator.java @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2020 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.paseto.validator; + +import io.micronaut.http.HttpRequest; +import io.micronaut.security.authentication.Authentication; +import io.micronaut.security.token.validator.TokenValidator; +import jakarta.inject.Singleton; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +/** + * @author Utsav Varia + * @since 3.0 + */ +@Singleton +public class PasetoTokenValidator implements TokenValidator { + + protected PasetoAuthenticationFactory pasetoAuthenticationFactory; + protected PasetoValidator validator; + + public PasetoTokenValidator(PasetoAuthenticationFactory pasetoAuthenticationFactory, PasetoValidator validator) { + this.pasetoAuthenticationFactory = pasetoAuthenticationFactory; + this.validator = validator; + } + + @Override + public Publisher validateToken(String token, HttpRequest request) { + return validator.validate(token, request) + .flatMap(pasetoAuthenticationFactory::createAuthentication) + .map(Flux::just) + .orElse(Flux.empty()); + } +} diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/PasetoValidator.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/PasetoValidator.java new file mode 100644 index 0000000000..cb01e28316 --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/PasetoValidator.java @@ -0,0 +1,76 @@ +/* + * Copyright 2017-2020 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.paseto.validator; + +import dev.paseto.jpaseto.*; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpRequest; +import io.micronaut.security.token.paseto.generator.PasetoTokenConfigurationProperties; +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; + +/** + * @author Utsav Varia + * @since 3.0 + */ +@Singleton +public class PasetoValidator { + + private static final Logger LOG = LoggerFactory.getLogger(PasetoValidator.class); + protected final PasetoTokenConfigurationProperties tokenConfigurationProperties; + + public PasetoValidator(PasetoTokenConfigurationProperties tokenConfigurationProperties) { + this.tokenConfigurationProperties = tokenConfigurationProperties; + } + + /** + * Returns Paseto Token validator based on configuration. + * + * @return Paseto Builder + */ + public PasetoParser getPasetoParser() { + PasetoParserBuilder parser = Pasetos.parserBuilder(); + if (Purpose.LOCAL.equals(tokenConfigurationProperties.getPurpose())) { + parser.setSharedSecret(tokenConfigurationProperties.getSecretKey()); + } else { + // TODO: Add Public Key config + } + return parser.build(); + } + + /** + * Validates the supplied token with any configurations and claim validators present. + * + * @param token The Paseto string + * @param request HTTP request + * @return An optional Paseto token if validation succeeds + */ + public Optional validate(String token, @Nullable HttpRequest request) { + try { + PasetoParser parser = getPasetoParser(); + Paseto paseto = parser.parse(token); + return Optional.of(paseto); + } catch (ClaimPasetoException e) { + if (LOG.isTraceEnabled()) { + LOG.trace("Failed to parse Paseto token: {}", e.getMessage()); + } + } + return Optional.empty(); + } +} diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/claims/GenericPasetoClaimsValidator.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/claims/GenericPasetoClaimsValidator.java new file mode 100644 index 0000000000..246728bbb9 --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/claims/GenericPasetoClaimsValidator.java @@ -0,0 +1,24 @@ +/* + * Copyright 2017-2020 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.paseto.validator.claims; + +/** + * @author Utsav Varia + * @since 3.0 + */ +public interface GenericPasetoClaimsValidator extends PasetoClaimsValidator { +} diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/claims/NotBeforePasetoClaimsValidator.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/claims/NotBeforePasetoClaimsValidator.java new file mode 100644 index 0000000000..1c0fa3e6f5 --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/claims/NotBeforePasetoClaimsValidator.java @@ -0,0 +1,72 @@ +/* + * Copyright 2017-2020 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.paseto.validator.claims; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.util.StringUtils; +import io.micronaut.http.HttpRequest; +import io.micronaut.security.token.paseto.generator.claims.PasetoClaims; +import io.micronaut.security.token.paseto.generator.claims.PasetoClaimsSet; +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Instant; + +/** + * Validate current time is not before the not-before claim of a Paseto token. + * + * @author Utsav Varia + * @since 3.0 + */ +@Singleton +@Requires(property = NotBeforePasetoClaimsValidator.NOT_BEFORE_PROP, value = StringUtils.TRUE) +public class NotBeforePasetoClaimsValidator implements GenericPasetoClaimsValidator { + + public static final String NOT_BEFORE_PROP = PasetoClaimsValidatorConfigurationProperties.PREFIX + ".not-before"; + + private static final Logger LOG = LoggerFactory.getLogger(NotBeforePasetoClaimsValidator.class); + + /** + * @param claimsSet The Paseto Claims + * @return true if the not-before claim denotes a date before now + */ + protected boolean validate(@NonNull PasetoClaimsSet claimsSet) { + final Instant notBefore = claimsSet.getNotBeforeTime(); + if (notBefore == null) { + return true; + } + + final Instant now = Instant.now(); + + if (now.isBefore(notBefore)) { + if (LOG.isTraceEnabled()) { + LOG.trace("Invalidating Paseto not-before Claim because current time ({}) is before ({}).", now, notBefore); + } + return false; + } + + return true; + } + + @Override + public boolean validate(@NonNull PasetoClaims claims, @Nullable HttpRequest request) { + return validate(PasetoClaimsSetUtils.pasetoClaimsSetFromClaims(claims)); + } +} diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/claims/PasetoClaimsSetUtils.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/claims/PasetoClaimsSetUtils.java new file mode 100644 index 0000000000..5bb2e5e46e --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/claims/PasetoClaimsSetUtils.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017-2020 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.paseto.validator.claims; + +import io.micronaut.security.token.paseto.generator.claims.PasetoClaims; +import io.micronaut.security.token.paseto.generator.claims.PasetoClaimsSet; + +/** + * @author Utsav Varia + * @since 3.0 + */ +public final class PasetoClaimsSetUtils { + + private PasetoClaimsSetUtils() { + } + + /** + * @param claims Paseto claims + * @return A PasetoClaimsSet + */ + public static PasetoClaimsSet pasetoClaimsSetFromClaims(PasetoClaims claims) { + PasetoClaimsSet.Builder claimsSetBuilder = new PasetoClaimsSet.Builder(); + for (String k : claims.names()) { + claimsSetBuilder.claim(k, claims.get(k)); + } + return claimsSetBuilder.build(); + } + +} diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/claims/PasetoClaimsValidator.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/claims/PasetoClaimsValidator.java new file mode 100644 index 0000000000..c4639c0853 --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/claims/PasetoClaimsValidator.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2020 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.paseto.validator.claims; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.HttpRequest; +import io.micronaut.security.token.paseto.generator.claims.PasetoClaims; + +/** + * @author Utsav Varia + * @since 3.0 + */ +public interface PasetoClaimsValidator { + /** + * @param claims JWT Claims + * @param request HTTP request + * @return whether the Paseto claims pass validation. + */ + boolean validate(@NonNull PasetoClaims claims, @Nullable HttpRequest request); + +} diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/claims/PasetoClaimsValidatorConfiguration.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/claims/PasetoClaimsValidatorConfiguration.java new file mode 100644 index 0000000000..f28585a310 --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/claims/PasetoClaimsValidatorConfiguration.java @@ -0,0 +1,53 @@ +/* + * Copyright 2017-2020 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.paseto.validator.claims; + +import io.micronaut.core.annotation.Nullable; + +/** + * @author Utsav Varia + * @since 3.0 + */ +public interface PasetoClaimsValidatorConfiguration { + + /** + * @return Whether the aud claim should be validated to ensure it matches this value. + */ + @Nullable + String getAudience(); + + /** + * @return Whether the iss claim should be validated to ensure it matches this value. + */ + @Nullable + String getIssuer(); + + /** + * @return Whether the Paseto subject claim should be validated to ensure it is not null. + */ + boolean isSubjectNotNull(); + + /** + * @return Whether it should be validated that validation time is not before the not-before claim (nbf) of a Paseto token. + */ + boolean isNotBefore(); + + /** + * @return Whether the expiration date of the Paseto should be validated. + */ + boolean isExpiration(); + +} diff --git a/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/claims/PasetoClaimsValidatorConfigurationProperties.java b/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/claims/PasetoClaimsValidatorConfigurationProperties.java new file mode 100644 index 0000000000..b545c6a968 --- /dev/null +++ b/security-paseto/src/main/java/io/micronaut/security/token/paseto/validator/claims/PasetoClaimsValidatorConfigurationProperties.java @@ -0,0 +1,125 @@ +/* + * Copyright 2017-2020 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.paseto.validator.claims; + +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.security.token.paseto.config.PasetoConfigurationProperties; + +/** + * {@link ConfigurationProperties} implementation of {@link PasetoClaimsValidatorConfiguration}. + * + * @author Utsav Varia + * @since 3.0 + */ +@ConfigurationProperties(PasetoClaimsValidatorConfigurationProperties.PREFIX) +public class PasetoClaimsValidatorConfigurationProperties implements PasetoClaimsValidatorConfiguration { + + public static final String PREFIX = PasetoConfigurationProperties.PREFIX + ".claims-validators"; + + /** + * The default expiration value. + */ + public static final boolean DEFAULT_EXPIRATION = true; + + /** + * The default subject-not-null value. + */ + public static final boolean DEFAULT_SUBJECT_NOT_NULL = true; + + /** + * The default not-before value. + */ + public static final boolean DEFAULT_NOT_BEFORE = false; + + @Nullable + private String audience; + + @Nullable + private String issuer; + + private boolean subjectNotNull = DEFAULT_SUBJECT_NOT_NULL; + + private boolean notBefore = DEFAULT_NOT_BEFORE; + + private boolean expiration = DEFAULT_EXPIRATION; + + @Override + @Nullable + public String getIssuer() { + return issuer; + } + + /** + * @param issuer Whether the iss claim should be validated to ensure it matches this value. It defaults to null, thus it is not validated. + */ + public void setIssuer(@Nullable String issuer) { + this.issuer = issuer; + } + + @Override + @Nullable + public String getAudience() { + return audience; + } + + /** + * @param audience Whether the aud claim should be validated to ensure it matches this value. It defaults to null, thus it is not validated. + */ + public void setAudience(@Nullable String audience) { + this.audience = audience; + } + + @Override + public boolean isSubjectNotNull() { + return subjectNotNull; + } + + /** + * @param subjectNotNull Whether the Paseto subject claim should be validated to ensure it is not null. Default value {@value #DEFAULT_SUBJECT_NOT_NULL}. + */ + public void setSubjectNotNull(boolean subjectNotNull) { + this.subjectNotNull = subjectNotNull; + } + + /** + * @return Whether it should be validated that validation time is not before the not-before claim (nbf) of a Paseto token. + */ + @Override + public boolean isNotBefore() { + return notBefore; + } + + /** + * @param notBefore Whether it should be validated that validation time is not before the not-before claim (nbf) of a Paseto token. Default value {@value #DEFAULT_NOT_BEFORE}. + */ + public void setNotBefore(boolean notBefore) { + this.notBefore = notBefore; + } + + @Override + public boolean isExpiration() { + return this.expiration; + } + + /** + * @param expiration Whether the expiration date of the Paseto should be validated. Default value {@value #DEFAULT_EXPIRATION}. + */ + public void setExpiration(boolean expiration) { + this.expiration = expiration; + } + +} diff --git a/security-paseto/src/test/groovy/io/micronaut/security/token/paseto/generator/claims/PasetoClaimsGeneratorSpec.groovy b/security-paseto/src/test/groovy/io/micronaut/security/token/paseto/generator/claims/PasetoClaimsGeneratorSpec.groovy new file mode 100644 index 0000000000..afe08d1c85 --- /dev/null +++ b/security-paseto/src/test/groovy/io/micronaut/security/token/paseto/generator/claims/PasetoClaimsGeneratorSpec.groovy @@ -0,0 +1,28 @@ +package io.micronaut.security.token.paseto.generator.claims + +import dev.paseto.jpaseto.Claims +import io.micronaut.security.authentication.Authentication +import io.micronaut.security.token.config.TokenConfiguration +import spock.lang.Specification + +class PasetoClaimsGeneratorSpec extends Specification { + def "generateClaims includes sub and exp claims"() { + given: + PasetoClaimsGenerator generator = new PasetoClaimsGenerator(new TokenConfiguration() {}, null, null, null) + + when: + Map claims = generator.generateClaims(Authentication.build('admin', ['ROLE_USER', 'ROLE_ADMIN']), 3600) + List expectedClaimsNames = [Claims.SUBJECT, + Claims.ISSUED_AT, + Claims.EXPIRATION, + Claims.NOT_BEFORE, + Claims.ISSUER, + "roles"] + then: + claims + claims.keySet().size() == expectedClaimsNames.size() + expectedClaimsNames.each { String claimName -> + assert claims.get(claimName) + } + } +} diff --git a/security-paseto/src/test/groovy/io/micronaut/security/token/paseto/validator/claims/NotBeforePasetoClaimsValidatorSpec.groovy b/security-paseto/src/test/groovy/io/micronaut/security/token/paseto/validator/claims/NotBeforePasetoClaimsValidatorSpec.groovy new file mode 100644 index 0000000000..c0e1baaed2 --- /dev/null +++ b/security-paseto/src/test/groovy/io/micronaut/security/token/paseto/validator/claims/NotBeforePasetoClaimsValidatorSpec.groovy @@ -0,0 +1,128 @@ +package io.micronaut.security.token.paseto.validator.claims + +import io.micronaut.context.ApplicationContext +import io.micronaut.core.annotation.NonNull +import io.micronaut.core.annotation.Nullable +import io.micronaut.security.authentication.Authentication +import io.micronaut.security.token.paseto.generator.PasetoTokenGenerator +import io.micronaut.security.token.paseto.generator.claims.PasetoClaims +import io.micronaut.security.token.paseto.validator.PasetoTokenValidator +import reactor.core.publisher.Flux +import spock.lang.Shared +import spock.lang.Specification + +import java.time.Instant + +class NotBeforePasetoClaimsValidatorSpec extends Specification { + @Shared + long anHour = 60 * 60 + + @Shared + long nextHour = Instant.now().getEpochSecond() + anHour + + @Shared + Instant future = Instant.ofEpochSecond(nextHour) + + @Shared + long lastHour = Instant.now().getEpochSecond() - anHour + + @Shared + Instant past = Instant.ofEpochSecond(lastHour) + + void "not-before claim valid if not-before date is in the past"() { + given: + ApplicationContext context = ApplicationContext.run([ + 'micronaut.security.token.paseto.config.purpose' : 'local', + 'micronaut.security.token.paseto.config.secretKey' : 'dGhpc0lzTXlTZWNyZXQ=', + 'micronaut.security.token.paseto.claims-validators.not-before': 'true' + ]) + String paseto = generatePasetoWithNotBefore(context, past) + + when: + Authentication result = authenticate(context, paseto) + + then: + result + result.attributes[PasetoClaims.SUBJECT] == "alice" + + cleanup: + context.close() + } + + void "not-before claim is not valid if not-before date is in the future"() { + given: + ApplicationContext context = ApplicationContext.run([ + 'micronaut.security.token.paseto.config.purpose' : 'local', + 'micronaut.security.token.paseto.config.secretKey' : 'dGhpc0lzTXlTZWNyZXQ=', + 'micronaut.security.token.paseto.claims-validators.not-before': 'true' + ]) + String paseto = generatePasetoWithNotBefore(context, future) + + when: + Authentication result = authenticate(context, paseto) + + then: + !result + + cleanup: + context.close() + } + + void "not-before claim is valid if token does not contain a not-before claim"() { + given: + ApplicationContext context = ApplicationContext.run([ + 'micronaut.security.token.paseto.config.purpose' : 'local', + 'micronaut.security.token.paseto.config.secretKey' : 'dGhpc0lzTXlTZWNyZXQ=', + 'micronaut.security.token.paseto.claims-validators.not-before': 'true' + ]) + String paseto = generatePasetoWithNotBefore(context, null) + + when: + Authentication result = authenticate(context, paseto) + + then: + result + result.attributes[PasetoClaims.SUBJECT] == "alice" + + cleanup: + context.close() + } + + void "not-before claim ignored if configuration prop not explicitly set to true"() { + given: + ApplicationContext context = ApplicationContext.run([ + 'micronaut.security.token.paseto.config.purpose' : 'local', + 'micronaut.security.token.paseto.config.secretKey': 'dGhpc0lzTXlTZWNyZXQ=' + ]) + String paseto = generatePasetoWithNotBefore(context, past) + + when: + Authentication result = authenticate(context, paseto) + + then: + result + result.attributes[PasetoClaims.SUBJECT] == "alice" + + cleanup: + context.close() + } + + + @Nullable + private static Authentication authenticate(@NonNull ApplicationContext context, @NonNull String paseto) { + PasetoTokenValidator pasetoTokenValidator = context.getBean(PasetoTokenValidator.class) + Flux.from(pasetoTokenValidator.validateToken(paseto, null)).blockFirst() + } + + + @NonNull + private static String generatePasetoWithNotBefore(ApplicationContext context, @Nullable Object notBefore) { + PasetoTokenGenerator pasetoTokenGenerator = context.getBean(PasetoTokenGenerator.class) + Map claims = [:] + claims[PasetoClaims.SUBJECT] = 'alice' + if (notBefore != null) { + claims[PasetoClaims.NOT_BEFORE] = notBefore + } + pasetoTokenGenerator.generateToken(claims).get() + } +} diff --git a/settings.gradle b/settings.gradle index 2d5403d525..6a307b83e0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,3 +23,5 @@ include "test-suite" include "test-suite-utils" include "test-suite-utils-security" include "test-suite-kotlin" +include 'security-paseto' +