🚧 Add refresh grant
This commit is contained in:
parent
308ec0c93a
commit
1aebd3b50c
@ -1,10 +1,16 @@
|
||||
package dev.dinauer.oidcproxy;
|
||||
|
||||
import dev.dinauer.oidcproxy.session.SessionCache;
|
||||
import dev.dinauer.oidcproxy.session.SessionService;
|
||||
import io.vertx.core.http.Cookie;
|
||||
import io.vertx.core.http.HttpServerRequest;
|
||||
import io.vertx.core.http.HttpServerResponse;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@ApplicationScoped
|
||||
public class LogoutService
|
||||
{
|
||||
@ -13,12 +19,33 @@ public class LogoutService
|
||||
@ConfigProperty(name = "oidc.proxy.logout.redirect.url")
|
||||
String logoutRedirectUrl;
|
||||
|
||||
public void logout(HttpServerResponse response)
|
||||
@Inject
|
||||
SessionCache sessionCache;
|
||||
|
||||
public void logout(HttpServerRequest request, HttpServerResponse response)
|
||||
{
|
||||
String sessionId = getSessionId(request.cookies());
|
||||
if (sessionId != null)
|
||||
{
|
||||
sessionCache.remove(sessionId);
|
||||
}
|
||||
|
||||
response.addCookie(Cookie.cookie("session", EMPTY).setMaxAge(0).setPath("/").setHttpOnly(true).setSecure(true));
|
||||
response.addCookie(Cookie.cookie("identity", EMPTY).setMaxAge(0).setPath("/").setHttpOnly(false).setSecure(true));
|
||||
response.setStatusCode(302);
|
||||
response.putHeader("Location", logoutRedirectUrl);
|
||||
response.send();
|
||||
}
|
||||
|
||||
private String getSessionId(Set<Cookie> cookies)
|
||||
{
|
||||
for (Cookie cookie : cookies)
|
||||
{
|
||||
if ("session".equals(cookie.getName()))
|
||||
{
|
||||
return cookie.getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,9 +47,9 @@ public class Resource
|
||||
|
||||
@Route(path = "/auth/logout", order = 1)
|
||||
@Blocking
|
||||
public void logout(@Context HttpServerResponse response)
|
||||
public void logout(@Context RoutingContext context)
|
||||
{
|
||||
logoutService.logout(response);
|
||||
logoutService.logout(context.request(), context.response());
|
||||
}
|
||||
|
||||
@Route(path = "/api/*", order = 2)
|
||||
|
||||
@ -30,7 +30,7 @@ public class CallbackService
|
||||
{
|
||||
String code = request.params().get("code");
|
||||
TokenResponse oidcResponse = client.exchangeAuthorizationCode(code);
|
||||
String sessionId = sessionCache.add(oidcResponse.accessToken(), oidcResponse.refreshToken(), Instant.ofEpochSecond(oidcResponse.expiresAt()).atZone(ZoneOffset.UTC));
|
||||
String sessionId = sessionCache.add(oidcResponse.accessToken(), oidcResponse.refreshToken());
|
||||
|
||||
int cookieExpiry = (int) (JwtUtils.extractExpiresAt(oidcResponse.refreshToken()).toEpochSecond() - ZonedDateTime.now().toEpochSecond());
|
||||
response.addCookie(Cookie.cookie("session", sessionId).setHttpOnly(true).setSecure(true).setPath("/").setMaxAge(cookieExpiry));
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package dev.dinauer.oidcproxy.callback;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import dev.dinauer.oidcproxy.callback.model.TokenResponse;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
@ -47,7 +49,38 @@ public class OidcClient
|
||||
}
|
||||
throw new RuntimeException();
|
||||
}
|
||||
catch (IOException | InterruptedException e)
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
catch (InterruptedException e)
|
||||
{
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public TokenResponse refreshAccessToken(String refreshToken)
|
||||
{
|
||||
String body = String.format("grant_type=refresh_token&refresh_token=%s", URLEncoder.encode(refreshToken, StandardCharsets.UTF_8));
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.method("POST", HttpRequest.BodyPublishers.ofString(body))
|
||||
.header("Content-Type", APPLICATION_URLENCODED)
|
||||
.header("Authorization", formatAuthHeader(clientId, clientSecret)).uri(URI.create(oidcAuthUrl)).build();
|
||||
try
|
||||
{
|
||||
HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() < 400)
|
||||
{
|
||||
return OBJECT_MAPPER.readValue(response.body(), TokenResponse.class);
|
||||
}
|
||||
throw new RuntimeException();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
catch (InterruptedException e)
|
||||
{
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException(e);
|
||||
|
||||
@ -2,6 +2,15 @@ package dev.dinauer.oidcproxy.session;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
public record AccessToken(ZonedDateTime expiresAt, String token)
|
||||
public class AccessToken extends OidcToken
|
||||
{
|
||||
public AccessToken(String token)
|
||||
{
|
||||
super(token);
|
||||
}
|
||||
|
||||
public AccessToken(String token, ZonedDateTime expiresAt)
|
||||
{
|
||||
super(token, expiresAt);
|
||||
}
|
||||
}
|
||||
|
||||
33
src/main/java/dev/dinauer/oidcproxy/session/OidcToken.java
Normal file
33
src/main/java/dev/dinauer/oidcproxy/session/OidcToken.java
Normal file
@ -0,0 +1,33 @@
|
||||
package dev.dinauer.oidcproxy.session;
|
||||
|
||||
import dev.dinauer.oidcproxy.JwtUtils;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
public abstract class OidcToken
|
||||
{
|
||||
private final String token;
|
||||
private final ZonedDateTime expiresAt;
|
||||
|
||||
public OidcToken(String token)
|
||||
{
|
||||
this.token = token;
|
||||
this.expiresAt = JwtUtils.extractExpiresAt(token);
|
||||
}
|
||||
|
||||
public OidcToken(String token, ZonedDateTime expiresAt)
|
||||
{
|
||||
this.token = token;
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
public ZonedDateTime getExpiresAt()
|
||||
{
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public String getToken()
|
||||
{
|
||||
return token;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package dev.dinauer.oidcproxy.session;
|
||||
|
||||
public class RefreshToken extends OidcToken
|
||||
{
|
||||
public RefreshToken(String token)
|
||||
{
|
||||
super(token);
|
||||
}
|
||||
}
|
||||
@ -56,36 +56,31 @@ public class SessionCache
|
||||
}, 0, 30, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public String add(String accessToken, String refreshToken, ZonedDateTime expiresAt)
|
||||
public String add(String accessToken, String refreshToken)
|
||||
{
|
||||
String sessionId = UUID.randomUUID().toString();
|
||||
sessionService.create(sessionId, expiresAt, accessToken, refreshToken);
|
||||
sessionService.create(toHash(sessionId), new AccessToken(accessToken), new RefreshToken(refreshToken));
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public String get(String sessionId) throws TokenNotFoundException
|
||||
{
|
||||
String session = toHash(sessionId);
|
||||
AccessToken token = tokens.get(session);
|
||||
String sessionHash = toHash(sessionId);
|
||||
AccessToken token = tokens.get(sessionHash);
|
||||
if (token != null)
|
||||
{
|
||||
return token.token();
|
||||
return token.getToken();
|
||||
}
|
||||
AccessTokenEntity accessTokenEntity = accessTokenRepository.findById(session);
|
||||
if (session != null)
|
||||
AccessToken fromDB = sessionService.provide(sessionHash);
|
||||
tokens.put(sessionHash, fromDB);
|
||||
return fromDB.getToken();
|
||||
}
|
||||
|
||||
public void remove(String sessionId)
|
||||
{
|
||||
try
|
||||
{
|
||||
String accessToken = encryptUtils.decrypt(accessTokenEntity.getToken());
|
||||
tokens.put(session, new AccessToken(accessTokenEntity.getExpiresAt(), accessToken));
|
||||
return accessToken;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
throw new TokenNotFoundException();
|
||||
String sessionHash = toHash(sessionId);
|
||||
tokens.remove(sessionHash);
|
||||
sessionService.remove(sessionHash);
|
||||
}
|
||||
|
||||
private String toHash(String sessionId)
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
package dev.dinauer.oidcproxy.session;
|
||||
|
||||
import dev.dinauer.oidcproxy.JwtUtils;
|
||||
import dev.dinauer.oidcproxy.callback.OidcClient;
|
||||
import dev.dinauer.oidcproxy.callback.model.TokenResponse;
|
||||
import dev.dinauer.oidcproxy.proxy.exception.TokenNotFoundException;
|
||||
import io.quarkus.security.UnauthorizedException;
|
||||
import io.smallrye.jwt.auth.principal.DefaultJWTParser;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
@ -8,6 +13,7 @@ import org.eclipse.microprofile.jwt.JsonWebToken;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.sql.Ref;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
@ -25,16 +31,16 @@ public class SessionService
|
||||
@Inject
|
||||
RefreshTokenRepository refreshTokenRepository;
|
||||
|
||||
@Inject
|
||||
OidcClient client;
|
||||
|
||||
@Transactional
|
||||
public void create(String sessionId, ZonedDateTime sessionExpiresAt, String accessToken, String refreshToken)
|
||||
public void create(String sessionHash, AccessToken accessToken, RefreshToken refreshToken)
|
||||
{
|
||||
String sessionHash = toHash(sessionId);
|
||||
try
|
||||
{
|
||||
JsonWebToken token = new DefaultJWTParser().parseOnly(accessToken);
|
||||
ZonedDateTime accessTokenExpiresAt = Instant.ofEpochSecond(token.getExpirationTime()).atZone(ZoneOffset.UTC);
|
||||
accessTokenRepository.persist(new AccessTokenEntity().setId(sessionHash).setToken(encryptUtils.encrypt(accessToken)).setExpiresAt(accessTokenExpiresAt));
|
||||
refreshTokenRepository.persist(new RefreshTokenEntity().setId(sessionHash).setToken(encryptUtils.encrypt(refreshToken)).setExpiresAt(sessionExpiresAt));
|
||||
accessTokenRepository.persist(new AccessTokenEntity().setId(sessionHash).setToken(encryptUtils.encrypt(accessToken.getToken())).setExpiresAt(accessToken.getExpiresAt()));
|
||||
refreshTokenRepository.persist(new RefreshTokenEntity().setId(sessionHash).setToken(encryptUtils.encrypt(refreshToken.getToken())).setExpiresAt(refreshToken.getExpiresAt()));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -42,15 +48,59 @@ public class SessionService
|
||||
}
|
||||
}
|
||||
|
||||
private String toHash(String sessionId)
|
||||
@Transactional
|
||||
public AccessToken provide(String sessionHash) throws TokenNotFoundException
|
||||
{
|
||||
AccessTokenEntity dbToken = accessTokenRepository.findById(sessionHash);
|
||||
if (isValid(dbToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
return Base64.getEncoder().encodeToString( MessageDigest.getInstance("SHA-256").digest(sessionId.getBytes()));
|
||||
return new AccessToken(encryptUtils.decrypt(dbToken.getToken()), dbToken.getExpiresAt());
|
||||
}
|
||||
catch (NoSuchAlgorithmException e)
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
RefreshTokenEntity dbRefreshToken = refreshTokenRepository.findById(sessionHash);
|
||||
if (dbRefreshToken != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
TokenResponse tokenResponse = client.refreshAccessToken(encryptUtils.decrypt(dbRefreshToken.getToken()));
|
||||
renewAccessToken(sessionHash, new AccessToken(tokenResponse.accessToken()));
|
||||
return new AccessToken(tokenResponse.accessToken());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new TokenNotFoundException();
|
||||
}
|
||||
|
||||
}
|
||||
throw new TokenNotFoundException();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void remove(String sessionHash)
|
||||
{
|
||||
accessTokenRepository.deleteById(sessionHash);
|
||||
refreshTokenRepository.deleteById(sessionHash);
|
||||
}
|
||||
|
||||
private boolean isValid(AccessTokenEntity token)
|
||||
{
|
||||
if (token != null)
|
||||
{
|
||||
return ZonedDateTime.now().isBefore(token.getExpiresAt());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void renewAccessToken(String sessionHash, AccessToken token) throws Exception
|
||||
{
|
||||
accessTokenRepository.deleteById(sessionHash);
|
||||
accessTokenRepository.persist(new AccessTokenEntity().setId(sessionHash).setToken(encryptUtils.encrypt(token.getToken())).setExpiresAt(token.getExpiresAt()));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,6 @@ oidc.proxy.client.redirect=http://localhost:3000
|
||||
|
||||
%dev,test.oidc.proxy.logout.redirect.url=http://localhost:3000
|
||||
|
||||
%dev,test.quarkus.flyway.clean-at-start=true
|
||||
%dev,test.quarkus.flyway.clean-at-start=false
|
||||
%dev.quarkus.flyway.locations=db/migration,db/dev
|
||||
quarkus.flyway.migrate-at-start=true
|
||||
Loading…
x
Reference in New Issue
Block a user