diff --git a/src/main/java/de/tavolio/auth/identityproviders/ClientIdentityProvider.java b/src/main/java/de/tavolio/auth/identityproviders/ClientIdentityProvider.java index 1e6e36d..1e83148 100644 --- a/src/main/java/de/tavolio/auth/identityproviders/ClientIdentityProvider.java +++ b/src/main/java/de/tavolio/auth/identityproviders/ClientIdentityProvider.java @@ -1,8 +1,5 @@ package de.tavolio.auth.identityproviders; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import de.tavolio.Role; import de.tavolio.auth.utils.JwtUtils; import de.tavolio.oidc.IssuerService; @@ -70,7 +67,7 @@ public class ClientIdentityProvider implements IdentityProvider KeypairEntity keypairEntity = keypairRepo.findById(kid); if (keypairEntity != null) { - PublicKey publicKey = jwksService.findByKid(keypairEntity).toPublicKey(); + PublicKey publicKey = jwksService.generate(keypairEntity).toPublicKey(); RealmEntity realm = keypairEntity.getRealm(); try { diff --git a/src/main/java/de/tavolio/oidc/OidcResource.java b/src/main/java/de/tavolio/oidc/OidcResource.java index 506f11c..edd59a3 100644 --- a/src/main/java/de/tavolio/oidc/OidcResource.java +++ b/src/main/java/de/tavolio/oidc/OidcResource.java @@ -17,6 +17,7 @@ import jakarta.inject.Inject; import jakarta.transaction.Transactional; import jakarta.ws.rs.*; import jakarta.ws.rs.core.Response; +import org.apache.commons.lang3.NotImplementedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -83,7 +84,7 @@ public class OidcResource @POST @Path("/token") @Transactional - public TokenResponse token(@FormParam("grant_type") String grantType, @FormParam("code") String code) throws NoSuchAlgorithmException, InvalidKeySpecException + public TokenResponse token(@FormParam("grant_type") String grantType, @FormParam("code") String code, @FormParam("refresh_token") String refreshToken) throws NoSuchAlgorithmException, InvalidKeySpecException { Grant grant = getGrant(grantType); @@ -103,6 +104,14 @@ public class OidcResource { return clientTokenService.getToken(realmKey); } + if (Grant.PASSWORD.equals(grant)) + { + throw new NotImplementedException(); + } + if (Grant.REFRESH_TOKEN.equals(grant)) + { + return userTokenService.refreshAccessToken(realmKey, refreshToken); + } throw new RuntimeException(); } @@ -116,6 +125,14 @@ public class OidcResource { return Grant.CLIENT_CREDENTIALS; } + if ("refresh_token".equals(grantType)) + { + return Grant.REFRESH_TOKEN; + } + if ("password".equals(grantType)) + { + return Grant.PASSWORD; + } LOG.error("Invalid grant {} provided", grantType); throw new RuntimeException(); } diff --git a/src/main/java/de/tavolio/oidc/token/UserTokenGenerator.java b/src/main/java/de/tavolio/oidc/token/UserTokenGenerator.java index ec0f100..20554d5 100644 --- a/src/main/java/de/tavolio/oidc/token/UserTokenGenerator.java +++ b/src/main/java/de/tavolio/oidc/token/UserTokenGenerator.java @@ -19,6 +19,7 @@ public class UserTokenGenerator return Jwt.claims() .upn(upn) .audience(clientId) + .claim("token_purpose", "access_token") .claim("realm_key", realmKey) .claim("client_id", clientId) .expiresAt(expiresAt.toInstant()) @@ -30,6 +31,7 @@ public class UserTokenGenerator { return Jwt.claims() .upn(upn) + .claim("token_purpose", "refresh_token") .claim("realm_key", realmKey) .claim("client_id", clientId) .expiresAt(expiresAt.toInstant()) diff --git a/src/main/java/de/tavolio/oidc/token/UserTokenService.java b/src/main/java/de/tavolio/oidc/token/UserTokenService.java index 2cab379..e2695f8 100644 --- a/src/main/java/de/tavolio/oidc/token/UserTokenService.java +++ b/src/main/java/de/tavolio/oidc/token/UserTokenService.java @@ -9,10 +9,14 @@ import de.tavolio.realm.code.CodeRepo; import de.tavolio.realm.RealmEntity; import de.tavolio.realm.RealmService; import de.tavolio.realm.key.KeypairEntity; +import de.tavolio.verify.JwtVerificationService; import io.quarkus.security.identity.SecurityIdentity; +import io.smallrye.jwt.auth.principal.DefaultJWTParser; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.*; +import org.apache.commons.lang3.Strings; +import org.eclipse.microprofile.jwt.JsonWebToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,6 +45,9 @@ public class UserTokenService @Inject RealmService realmService; + @Inject + JwtVerificationService jwtVerificationService; + public TokenResponse getToken(String realmKey, String code) { return generateUserToken(realmKey, code); @@ -73,4 +80,45 @@ public class UserTokenService } throw new BadRequestException(); } + + public TokenResponse refreshAccessToken(String realmKey, String refreshToken) + { + String principal = identity.getPrincipal().getName(); + + JsonWebToken token = jwtVerificationService.validate(realmKey, refreshToken); + + String clientId = token.getClaim("client_id"); + assertClientId(clientId, principal); + + String purpose = token.getClaim("token_purpose"); + assertTokenPurpose(purpose); + + RealmEntity realm = realmService.requireByKey(realmKey); + + KeypairEntity keypair = realm.getKeys().getFirst(); + String kid = keypair.getId(); + PrivateKey signingKey = KeypairEntity.toPrivateKey(keypair); + ZonedDateTime expiresAt = ZonedDateTime.now().plusSeconds(realm.getLifetime()); + return new TokenResponse() + .setTokenType("Bearer") + .setExpiresAt(expiresAt.toEpochSecond()) + .setAccessToken(userTokenGenerator.generateAccessToken(realm.getKey(), principal, token.getName(), expiresAt, signingKey, kid)) + .setRefreshToken(refreshToken); + } + + private void assertClientId(String tokenClientId, String requestingClientId) + { + if (!Strings.CI.equals(tokenClientId, requestingClientId)) + { + throw new RuntimeException("JWT client id and requesting client id do not match. Deny refresh."); + } + } + + private void assertTokenPurpose(String purpose) + { + if (!Strings.CI.equals("refresh_token", purpose)) + { + throw new RuntimeException("Provided token is not a refresh token. Please provide a refresh token."); + } + } } diff --git a/src/main/java/de/tavolio/realm/client/Grant.java b/src/main/java/de/tavolio/realm/client/Grant.java index 938fa3b..4cd9018 100644 --- a/src/main/java/de/tavolio/realm/client/Grant.java +++ b/src/main/java/de/tavolio/realm/client/Grant.java @@ -2,5 +2,5 @@ package de.tavolio.realm.client; public enum Grant { - CLIENT_CREDENTIALS, AUTHORIZATION_CODE, PASSWORD + CLIENT_CREDENTIALS, AUTHORIZATION_CODE, PASSWORD, REFRESH_TOKEN } diff --git a/src/main/java/de/tavolio/verify/JwksService.java b/src/main/java/de/tavolio/verify/JwksService.java index c5fb511..ffef290 100644 --- a/src/main/java/de/tavolio/verify/JwksService.java +++ b/src/main/java/de/tavolio/verify/JwksService.java @@ -2,24 +2,19 @@ package de.tavolio.verify; import de.tavolio.realm.key.KeypairEntity; import de.tavolio.realm.key.KeypairRepo; -import de.tavolio.realm.RealmEntity; import de.tavolio.verify.jwks.EcPublicKey; import de.tavolio.verify.jwks.JwksKey; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.apache.commons.lang3.NotImplementedException; -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; - @ApplicationScoped public class JwksService { @Inject KeypairRepo keypairRepo; - public JwksKey findByKid(KeypairEntity keypair) + public JwksKey generate(KeypairEntity keypair) { switch (keypair.getType()) { diff --git a/src/main/java/de/tavolio/verify/JwtVerificationService.java b/src/main/java/de/tavolio/verify/JwtVerificationService.java new file mode 100644 index 0000000..7f5f3d9 --- /dev/null +++ b/src/main/java/de/tavolio/verify/JwtVerificationService.java @@ -0,0 +1,51 @@ +package de.tavolio.verify; + +import de.tavolio.auth.utils.JwtUtils; +import de.tavolio.oidc.IssuerService; +import de.tavolio.realm.key.KeypairEntity; +import de.tavolio.realm.key.KeypairRepo; +import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.UnauthorizedException; +import io.smallrye.jwt.auth.principal.DefaultJWTParser; +import io.smallrye.jwt.auth.principal.JWTAuthContextInfo; +import io.smallrye.jwt.auth.principal.ParseException; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.eclipse.microprofile.jwt.JsonWebToken; + +@ApplicationScoped +public class JwtVerificationService +{ + @Inject + IssuerService issuerService; + + @Inject + JwksService jwksService; + + @Inject + KeypairRepo keypairRepo; + + public JsonWebToken validate(String realmKey, String jwt) + { + KeypairEntity keypair = keypairRepo.findById(JwtUtils.parseHeader(jwt).getKid()); + if (keypair != null) + { + try + { + return new DefaultJWTParser(getContextForRealm(realmKey)).verify(jwt, jwksService.generate(keypair).toPublicKey()); + } + catch (ParseException e) + { + throw new UnauthorizedException(); + } + } + throw new UnauthorizedException(); + } + + private JWTAuthContextInfo getContextForRealm(String realmKey) + { + JWTAuthContextInfo info = new JWTAuthContextInfo(); + info.setIssuedBy(issuerService.getIssuer(realmKey)); + return info; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 31e8761..79a3011 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,8 +1,12 @@ +quarkus.jvm-args=--add-opens java.base/java.lang=ALL-UNNAMED + quarkus.http.root-path=/api quarkus.http.test-port=9089 quarkus.http.host=0.0.0.0 %dev.quarkus.http.port=8089 +quarkus.http.access-log.enabled=true + quarkus.http.cors.enabled=true %dev.quarkus.http.cors.origins=/.*/ diff --git a/src/main/resources/bootstrap.yaml b/src/main/resources/bootstrap.yaml index b7ef9e0..7940abd 100644 --- a/src/main/resources/bootstrap.yaml +++ b/src/main/resources/bootstrap.yaml @@ -22,6 +22,7 @@ realms: allowed-grants: - AUTHORIZATION_CODE - CLIENT_CREDENTIALS + - REFRESH_TOKEN analytics-backend: secret: bcrypt: $2a$12$1oYS45e/nXP1OeMgdZZAKeEixarRDzbBGZd0xOnEQQMKlOKwVMrX. diff --git a/src/main/resources/db/migration/V1.0.1__init.sql b/src/main/resources/db/migration/V1.0.1__init.sql index 2cfb1ab..a790d7c 100755 --- a/src/main/resources/db/migration/V1.0.1__init.sql +++ b/src/main/resources/db/migration/V1.0.1__init.sql @@ -29,7 +29,7 @@ create table allowed_grants client_id varchar(255) not null constraint fkpuqnyqud9ft1tn18qrcy7457c references client, grant_name varchar(255) - constraint allowed_grants_grant_name_check check ((grant_name)::text = ANY ((ARRAY ['CLIENT_CREDENTIALS':: character varying, 'AUTHORIZATION_CODE':: character varying, 'PASSWORD':: character varying])::text[]) + constraint allowed_grants_grant_name_check check ((grant_name)::text = ANY ((ARRAY ['CLIENT_CREDENTIALS':: character varying, 'AUTHORIZATION_CODE':: character varying, 'PASSWORD':: character varying, 'REFRESH_TOKEN':: character varying])::text[]) ) );