🚧 Add refresh grant
This commit is contained in:
parent
63d21eaf39
commit
6804ebd46a
@ -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<JWTRequest>
|
||||
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
|
||||
{
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,5 +2,5 @@ package de.tavolio.realm.client;
|
||||
|
||||
public enum Grant
|
||||
{
|
||||
CLIENT_CREDENTIALS, AUTHORIZATION_CODE, PASSWORD
|
||||
CLIENT_CREDENTIALS, AUTHORIZATION_CODE, PASSWORD, REFRESH_TOKEN
|
||||
}
|
||||
|
||||
@ -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())
|
||||
{
|
||||
|
||||
51
src/main/java/de/tavolio/verify/JwtVerificationService.java
Normal file
51
src/main/java/de/tavolio/verify/JwtVerificationService.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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=/.*/
|
||||
|
||||
|
||||
@ -22,6 +22,7 @@ realms:
|
||||
allowed-grants:
|
||||
- AUTHORIZATION_CODE
|
||||
- CLIENT_CREDENTIALS
|
||||
- REFRESH_TOKEN
|
||||
analytics-backend:
|
||||
secret:
|
||||
bcrypt: $2a$12$1oYS45e/nXP1OeMgdZZAKeEixarRDzbBGZd0xOnEQQMKlOKwVMrX.
|
||||
|
||||
@ -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[])
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user