mirror of
https://github.com/ysoftdevs/oauth-playground-server.git
synced 2026-01-18 17:47:21 +01:00
WIP scopes screen and session management
This commit is contained in:
5
pom.xml
5
pom.xml
@@ -50,6 +50,11 @@
|
||||
<artifactId>rest-assured</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.4</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package com.ysoft.geecon;
|
||||
|
||||
import com.ysoft.geecon.dto.AuthParams;
|
||||
import com.ysoft.geecon.dto.OAuthClient;
|
||||
import com.ysoft.geecon.dto.User;
|
||||
import com.ysoft.geecon.error.OAuthException;
|
||||
import com.ysoft.geecon.repo.ClientsRepo;
|
||||
import com.ysoft.geecon.repo.SessionsRepo;
|
||||
import com.ysoft.geecon.repo.UsersRepo;
|
||||
import io.quarkus.qute.CheckedTemplate;
|
||||
import io.quarkus.qute.TemplateInstance;
|
||||
@@ -11,9 +15,7 @@ import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriBuilder;
|
||||
import org.jboss.resteasy.reactive.RestQuery;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
@Path("/auth")
|
||||
@@ -21,125 +23,78 @@ public class OAuthResource {
|
||||
|
||||
@CheckedTemplate
|
||||
public static class Templates {
|
||||
public static native TemplateInstance login(String loginHint, String error);
|
||||
public static native TemplateInstance login(String loginHint, String sessionId, String error);
|
||||
|
||||
public static native TemplateInstance consents(List<String> scopes, String error);
|
||||
public static native TemplateInstance consents(User user, OAuthClient client, List<String> scopes, String sessionId, String error);
|
||||
}
|
||||
|
||||
@Inject
|
||||
ClientsRepo clientsRepo;
|
||||
@Inject
|
||||
UsersRepo usersRepo;
|
||||
@Inject
|
||||
SessionsRepo sessionsRepo;
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.TEXT_HTML)
|
||||
public TemplateInstance get(AuthParams params) {
|
||||
validateClient(params);
|
||||
|
||||
return Templates.login(params.loginHint, "");
|
||||
var client = validateClient(params);
|
||||
String sessionId = sessionsRepo.newAuthorizationSession(params, client);
|
||||
return Templates.login(params.getLoginHint(), sessionId, "");
|
||||
}
|
||||
|
||||
@POST
|
||||
@Produces(MediaType.TEXT_HTML)
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public Object post(AuthParams params,
|
||||
@FormParam("sessionId") String sessionId,
|
||||
@FormParam("username") String username,
|
||||
@FormParam("password") String password) {
|
||||
validateClient(params);
|
||||
var user = usersRepo.getUser(username);
|
||||
if (user.isEmpty()) {
|
||||
return Templates.login(username, "invalid_credentials");
|
||||
}
|
||||
if (!user.get().validatePassword(password)) {
|
||||
return Templates.login(username, "invalid_credentials");
|
||||
@FormParam("password") String password,
|
||||
@FormParam("scope") List<String> scopes) {
|
||||
|
||||
|
||||
var session = sessionsRepo.getSession(sessionId).orElseThrow(() -> new OAuthException("Invalid session"));
|
||||
if (session.user() == null) {
|
||||
var user = validateUser(username, password);
|
||||
if (user == null) {
|
||||
return Templates.login(username, sessionId, "invalid_credentials");
|
||||
} else {
|
||||
session = sessionsRepo.assignUser(sessionId, user);
|
||||
}
|
||||
}
|
||||
|
||||
return Response.seeOther(UriBuilder.fromUri(params.redirectUri)
|
||||
.queryParam("code", "randomCode")
|
||||
.queryParam("state", params.state)
|
||||
.build())
|
||||
if (session.acceptedScopes() == null) {
|
||||
if (scopes == null || scopes.isEmpty()) {
|
||||
return Templates.consents(session.user(), session.client(), session.params().getScopes(), sessionId, "");
|
||||
}
|
||||
}
|
||||
|
||||
String authCode = sessionsRepo.finishSession(sessionId, scopes);
|
||||
return Response.seeOther(UriBuilder.fromUri(params.getRedirectUri())
|
||||
.queryParam("code", authCode)
|
||||
.queryParam("state", params.getState())
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private User validateUser(String username, String password) {
|
||||
return usersRepo.getUser(username)
|
||||
.filter(u -> u.validatePassword(password)).orElse(null);
|
||||
|
||||
}
|
||||
|
||||
private OAuthClient validateClient(AuthParams params) {
|
||||
var client = clientsRepo.getClient(params.clientId)
|
||||
var client = clientsRepo.getClient(params.getClientId())
|
||||
.orElseThrow(() -> new RuntimeException("Not a valid client"));
|
||||
if (!client.validateRedirectUri(params.redirectUri)) {
|
||||
if (!client.validateRedirectUri(params.getRedirectUri())) {
|
||||
throw new RuntimeException("Invalid redirect URI");
|
||||
}
|
||||
if (StringUtil.isNullOrEmpty(params.state)) {
|
||||
if (StringUtil.isNullOrEmpty(params.getState())) {
|
||||
throw new RuntimeException("Invalid state");
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
public static class AuthParams {
|
||||
public enum ResponseType {
|
||||
code
|
||||
}
|
||||
|
||||
@RestQuery("login_hint")
|
||||
String loginHint;
|
||||
@RestQuery("response_type")
|
||||
ResponseType responseType;
|
||||
@RestQuery("client_id")
|
||||
String clientId;
|
||||
@RestQuery("redirect_uri")
|
||||
String redirectUri;
|
||||
@RestQuery("scope")
|
||||
String scope;
|
||||
@RestQuery("state")
|
||||
String state;
|
||||
|
||||
public String getLoginHint() {
|
||||
return loginHint;
|
||||
}
|
||||
|
||||
public void setLoginHint(String loginHint) {
|
||||
this.loginHint = loginHint;
|
||||
}
|
||||
|
||||
public ResponseType getResponseType() {
|
||||
return responseType;
|
||||
}
|
||||
|
||||
public void setResponseType(ResponseType responseType) {
|
||||
this.responseType = responseType;
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public void setClientId(String clientId) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
public String getRedirectUri() {
|
||||
return redirectUri;
|
||||
}
|
||||
|
||||
public void setRedirectUri(String redirectUri) {
|
||||
this.redirectUri = redirectUri;
|
||||
}
|
||||
|
||||
public String getScope() {
|
||||
return scope;
|
||||
}
|
||||
|
||||
public void setScope(String scope) {
|
||||
this.scope = scope;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(String state) {
|
||||
this.state = state;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.ysoft.geecon.dto;
|
||||
|
||||
public record AccessTokenResponse(String token, String scope, String idToken, long expiresIn) {
|
||||
}
|
||||
77
src/main/java/com/ysoft/geecon/dto/AuthParams.java
Normal file
77
src/main/java/com/ysoft/geecon/dto/AuthParams.java
Normal file
@@ -0,0 +1,77 @@
|
||||
package com.ysoft.geecon.dto;
|
||||
|
||||
import org.jboss.resteasy.reactive.RestQuery;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class AuthParams {
|
||||
public enum ResponseType {
|
||||
code
|
||||
}
|
||||
|
||||
@RestQuery("login_hint")
|
||||
String loginHint;
|
||||
@RestQuery("response_type")
|
||||
ResponseType responseType;
|
||||
@RestQuery("client_id")
|
||||
String clientId;
|
||||
@RestQuery("redirect_uri")
|
||||
String redirectUri;
|
||||
@RestQuery("scope")
|
||||
String scope;
|
||||
@RestQuery("state")
|
||||
String state;
|
||||
|
||||
public String getLoginHint() {
|
||||
return loginHint;
|
||||
}
|
||||
|
||||
public void setLoginHint(String loginHint) {
|
||||
this.loginHint = loginHint;
|
||||
}
|
||||
|
||||
public ResponseType getResponseType() {
|
||||
return responseType;
|
||||
}
|
||||
|
||||
public void setResponseType(ResponseType responseType) {
|
||||
this.responseType = responseType;
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public void setClientId(String clientId) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
public String getRedirectUri() {
|
||||
return redirectUri;
|
||||
}
|
||||
|
||||
public void setRedirectUri(String redirectUri) {
|
||||
this.redirectUri = redirectUri;
|
||||
}
|
||||
|
||||
public String getScope() {
|
||||
return scope;
|
||||
}
|
||||
|
||||
public List<String> getScopes() {
|
||||
return scope == null ? List.of() : Arrays.stream(scope.split(" ")).toList();
|
||||
}
|
||||
|
||||
public void setScope(String scope) {
|
||||
this.scope = scope;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(String state) {
|
||||
this.state = state;
|
||||
}
|
||||
}
|
||||
17
src/main/java/com/ysoft/geecon/dto/AuthorizationSession.java
Normal file
17
src/main/java/com/ysoft/geecon/dto/AuthorizationSession.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.ysoft.geecon.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record AuthorizationSession(AuthParams params, OAuthClient client, User user, List<String> acceptedScopes) {
|
||||
public AuthorizationSession(AuthParams params, OAuthClient client) {
|
||||
this(params, client, null, null);
|
||||
}
|
||||
|
||||
public AuthorizationSession withUser(User user) {
|
||||
return new AuthorizationSession(params, client, user, acceptedScopes);
|
||||
}
|
||||
|
||||
public AuthorizationSession withScopes(List<String> acceptedScopes) {
|
||||
return new AuthorizationSession(params, client, user, acceptedScopes);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.ysoft.geecon.dto;
|
||||
|
||||
public record OAuthClient(String clientId, String clientSecret, String redirectUri) {
|
||||
public record OAuthClient(String clientId, String description, String clientSecret, String redirectUri) {
|
||||
public boolean validateRedirectUri(String redirectUri) {
|
||||
return this.redirectUri != null && this.redirectUri.equals(redirectUri);
|
||||
}
|
||||
|
||||
7
src/main/java/com/ysoft/geecon/error/OAuthException.java
Normal file
7
src/main/java/com/ysoft/geecon/error/OAuthException.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package com.ysoft.geecon.error;
|
||||
|
||||
public class OAuthException extends RuntimeException {
|
||||
public OAuthException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ public class ClientsRepo {
|
||||
private final Map<String, OAuthClient> clients = new HashMap<>();
|
||||
|
||||
public ClientsRepo() {
|
||||
register(new OAuthClient("my-public-client", null, "https://localhost:8888/oauth_success"));
|
||||
register(new OAuthClient("my-public-client", "Example public client", null, "https://localhost:8888/oauth_success"));
|
||||
}
|
||||
|
||||
public Optional<OAuthClient> getClient(String clientId) {
|
||||
|
||||
13
src/main/java/com/ysoft/geecon/repo/SecureRandomStrings.java
Normal file
13
src/main/java/com/ysoft/geecon/repo/SecureRandomStrings.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.ysoft.geecon.repo;
|
||||
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Random;
|
||||
|
||||
public class SecureRandomStrings {
|
||||
private static final Random RANDOM = new SecureRandom();
|
||||
public static String alphanumeric(int length) {
|
||||
return RandomStringUtils.random(length, 0, 0, true, true, null, RANDOM);
|
||||
}
|
||||
}
|
||||
40
src/main/java/com/ysoft/geecon/repo/SessionsRepo.java
Normal file
40
src/main/java/com/ysoft/geecon/repo/SessionsRepo.java
Normal file
@@ -0,0 +1,40 @@
|
||||
package com.ysoft.geecon.repo;
|
||||
|
||||
import com.ysoft.geecon.dto.*;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@ApplicationScoped
|
||||
public class SessionsRepo {
|
||||
private final Map<String, AuthorizationSession> authorizationSessions = new HashMap<>();
|
||||
private final Map<String, String> sessionsByAuthorizationCode = new HashMap<>();
|
||||
|
||||
public Optional<AuthorizationSession> getSession(String sessionId) {
|
||||
return Optional.ofNullable(authorizationSessions.get(sessionId));
|
||||
}
|
||||
|
||||
public String newAuthorizationSession(AuthParams params, OAuthClient client) {
|
||||
var id = SecureRandomStrings.alphanumeric(10);
|
||||
authorizationSessions.put(id, new AuthorizationSession(params, client));
|
||||
return id;
|
||||
}
|
||||
|
||||
public String finishSession(String sessionId, List<String> acceptedScopes) {
|
||||
Objects.requireNonNull(authorizationSessions.computeIfPresent(sessionId, (id, s) -> s.withScopes(acceptedScopes)));
|
||||
var authCode = SecureRandomStrings.alphanumeric(10);
|
||||
sessionsByAuthorizationCode.put(authCode, sessionId);
|
||||
return authCode;
|
||||
}
|
||||
|
||||
|
||||
public AuthorizationSession assignUser(String sessionId, User user) {
|
||||
return Objects.requireNonNull(authorizationSessions.computeIfPresent(sessionId, (id, session) -> session.withUser(user)));
|
||||
}
|
||||
|
||||
public Optional<AuthorizationSession> redeemAuthorizationCode(String authorizationCode) {
|
||||
var sessionId = Optional.ofNullable(sessionsByAuthorizationCode.get(authorizationCode));
|
||||
sessionId.ifPresent(_id -> sessionsByAuthorizationCode.remove(authorizationCode));
|
||||
return sessionId.map(authorizationSessions::get);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login-container h2 {
|
||||
@@ -70,11 +70,21 @@
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<h2>Consents</h2>
|
||||
{#if error == "invalid_credentials"}
|
||||
<div class="error-popup" id="error-popup">Invalid credentials</div>
|
||||
{/if}
|
||||
<p>Hello <strong>{user.login}</strong></p>
|
||||
|
||||
<p>You are authorizing to <strong>{client.description}</strong>, which requests the following permissions from you:</p>
|
||||
|
||||
<form action="" method="post">
|
||||
<button type="submit" class="login-button">Login</button>
|
||||
<input type="hidden" name="sessionId" value="{sessionId}">
|
||||
<ul>
|
||||
{#each scopes}
|
||||
<li>
|
||||
<label><input type="checkbox" name="scope" value="{it}" checked>{it}</label>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<button type="submit" class="login-button">Confirm</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login-container h2 {
|
||||
@@ -74,6 +74,7 @@
|
||||
<div class="error-popup" id="error-popup">Invalid credentials</div>
|
||||
{/if}
|
||||
<form action="" method="post">
|
||||
<input type="hidden" name="sessionId" value="{sessionId}">
|
||||
<div class="form-group">
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" id="username" name="username" value="{loginHint}" required>
|
||||
|
||||
Reference in New Issue
Block a user