mirror of
https://github.com/ysoftdevs/oauth-playground-server.git
synced 2026-03-22 00:59:51 +01:00
Device Auth. Grant - the web browser part
This commit is contained in:
@@ -14,17 +14,31 @@ import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriBuilder;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Path("/auth")
|
||||
public class OAuthResource {
|
||||
|
||||
@CheckedTemplate
|
||||
public static class Templates {
|
||||
public static native TemplateInstance login(String loginHint, String sessionId, String error);
|
||||
@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,
|
||||
@FormParam("scope") List<String> scopes) {
|
||||
|
||||
public static native TemplateInstance consents(User user, OAuthClient client, List<String> scopes, String sessionId, String error);
|
||||
|
||||
sessionsRepo.getSession(sessionId).orElseThrow(() -> new OAuthException("Invalid session"));
|
||||
var user = validateUser(username, password);
|
||||
if (user == null) {
|
||||
return Templates.login(username, sessionId, "invalid_credentials");
|
||||
} else {
|
||||
AuthorizationSession session = sessionsRepo.assignUser(sessionId, user);
|
||||
return Templates.consents(session.user(), session.client(), session.params().getScopes(), sessionId, "");
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
@@ -43,49 +57,56 @@ public class OAuthResource {
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("consent")
|
||||
@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,
|
||||
@FormParam("scope") List<String> scopes) {
|
||||
public Response postConsent(
|
||||
@FormParam("sessionId") String sessionId,
|
||||
@FormParam("scope") List<String> scopes) {
|
||||
|
||||
sessionsRepo.getSession(sessionId).orElseThrow(() -> new OAuthException("Invalid session"));
|
||||
var session = sessionsRepo.authorizeSession(sessionId, 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);
|
||||
String redirectUri = session.params().getRedirectUri();
|
||||
if (StringUtils.isNotBlank(redirectUri)) {
|
||||
var responseTypes = session.params().getResponseTypes();
|
||||
|
||||
UriBuilder uri = UriBuilder.fromUri(redirectUri)
|
||||
.fragment("")
|
||||
.queryParam("state", session.params().getState());
|
||||
|
||||
if (responseTypes.contains(AuthParams.ResponseType.code)) {
|
||||
uri.queryParam("code", sessionsRepo.generateAuthorizationCode(sessionId));
|
||||
}
|
||||
}
|
||||
|
||||
if (session.acceptedScopes() == null) {
|
||||
if (scopes == null || scopes.isEmpty()) {
|
||||
return Templates.consents(session.user(), session.client(), session.params().getScopes(), sessionId, "");
|
||||
if (responseTypes.contains(AuthParams.ResponseType.token)) {
|
||||
uri.queryParam("access_token", session.tokens().accessToken());
|
||||
}
|
||||
if (responseTypes.contains(AuthParams.ResponseType.id_token)) {
|
||||
uri.queryParam("id_token", session.tokens().idToken());
|
||||
}
|
||||
return Response.seeOther(uri.build()).build();
|
||||
} else {
|
||||
return Response.ok(Templates.loginSuccess()).build();
|
||||
}
|
||||
}
|
||||
|
||||
session = sessionsRepo.authorizeSession(sessionId, scopes);
|
||||
@POST
|
||||
@Path("/device")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public DeviceResponse device(DeviceParams params) {
|
||||
var client = validateClient(params);
|
||||
AuthParams authParams = new AuthParams();
|
||||
authParams.setClientId(params.getClientId());
|
||||
String sessionId = sessionsRepo.newAuthorizationSession(authParams, client);
|
||||
|
||||
var responseTypes = params.getResponseTypes();
|
||||
|
||||
UriBuilder uri = UriBuilder.fromUri(params.getRedirectUri())
|
||||
.fragment("")
|
||||
.queryParam("state", params.getState());
|
||||
|
||||
if (responseTypes.contains(AuthParams.ResponseType.code)) {
|
||||
uri.queryParam("code", sessionsRepo.generateAuthorizationCode(sessionId));
|
||||
}
|
||||
if (responseTypes.contains(AuthParams.ResponseType.token)) {
|
||||
uri.queryParam("access_token", session.tokens().accessToken());
|
||||
}
|
||||
if (responseTypes.contains(AuthParams.ResponseType.id_token)) {
|
||||
uri.queryParam("id_token", session.tokens().idToken());
|
||||
}
|
||||
return Response.seeOther(uri.build()).build();
|
||||
return new DeviceResponse(
|
||||
sessionsRepo.generateAuthorizationCode(sessionId),
|
||||
sessionsRepo.generateUserCode(sessionId),
|
||||
"http://verificationuri/device-login",
|
||||
10,
|
||||
180
|
||||
);
|
||||
}
|
||||
|
||||
@POST
|
||||
@@ -99,6 +120,28 @@ public class OAuthResource {
|
||||
};
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/device-login")
|
||||
@Produces(MediaType.TEXT_HTML)
|
||||
public TemplateInstance enterDeviceCode() {
|
||||
return Templates.deviceLogin("");
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/device-login")
|
||||
@Produces(MediaType.TEXT_HTML)
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public Response postDeviceCode(@FormParam("code") String code) {
|
||||
return sessionsRepo.redeemUserCode(code)
|
||||
.map(session -> Response.ok(Templates.login(session.params().getLoginHint(), session.sessionId(), "")))
|
||||
.orElse(Response.status(404).entity(Templates.deviceLogin("invalid_code"))).build();
|
||||
}
|
||||
|
||||
private OAuthClient validateClient(DeviceParams params) {
|
||||
return clientsRepo.getClient(params.getClientId())
|
||||
.orElseThrow(() -> new RuntimeException("Not a valid client"));
|
||||
}
|
||||
|
||||
private AccessTokenResponse redeemAuthorizationCode(TokenParams params) {
|
||||
validateClient(params);
|
||||
var session = sessionsRepo.redeemAuthorizationCode(params.getCode())
|
||||
@@ -147,6 +190,17 @@ public class OAuthResource {
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
@CheckedTemplate
|
||||
public static class Templates {
|
||||
public static native TemplateInstance login(String loginHint, String sessionId, String error);
|
||||
|
||||
public static native TemplateInstance loginSuccess();
|
||||
|
||||
public static native TemplateInstance consents(User user, OAuthClient client, List<String> scopes, String sessionId, String error);
|
||||
|
||||
public static native TemplateInstance deviceLogin(String error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,21 +4,22 @@ import com.ysoft.geecon.repo.SecureRandomStrings;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record AuthorizationSession(AuthParams params,
|
||||
public record AuthorizationSession(String sessionId,
|
||||
AuthParams params,
|
||||
OAuthClient client,
|
||||
User user,
|
||||
List<String> acceptedScopes,
|
||||
AccessTokenResponse tokens) {
|
||||
public AuthorizationSession(AuthParams params, OAuthClient client) {
|
||||
this(params, client, null, null, null);
|
||||
this(SecureRandomStrings.alphanumeric(50), params, client, null, null, null);
|
||||
}
|
||||
|
||||
public AuthorizationSession withUser(User user) {
|
||||
return new AuthorizationSession(params, client, user, acceptedScopes, tokens);
|
||||
return new AuthorizationSession(sessionId, params, client, user, acceptedScopes, tokens);
|
||||
}
|
||||
|
||||
public AuthorizationSession withScopes(List<String> acceptedScopes) {
|
||||
return new AuthorizationSession(params, client, user, acceptedScopes, tokens);
|
||||
return new AuthorizationSession(sessionId, params, client, user, acceptedScopes, tokens);
|
||||
}
|
||||
|
||||
public AuthorizationSession withGeneratedTokens() {
|
||||
@@ -30,7 +31,7 @@ public record AuthorizationSession(AuthParams params,
|
||||
SecureRandomStrings.alphanumeric(50),
|
||||
idToken
|
||||
);
|
||||
return new AuthorizationSession(params, client, user, acceptedScopes, tokens);
|
||||
return new AuthorizationSession(sessionId, params, client, user, acceptedScopes, tokens);
|
||||
}
|
||||
|
||||
public String scope() {
|
||||
|
||||
18
src/main/java/com/ysoft/geecon/dto/DeviceParams.java
Normal file
18
src/main/java/com/ysoft/geecon/dto/DeviceParams.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.ysoft.geecon.dto;
|
||||
|
||||
import jakarta.ws.rs.FormParam;
|
||||
|
||||
public final class DeviceParams {
|
||||
@FormParam("client_id")
|
||||
String clientId;
|
||||
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public AuthParams toAuthParams() {
|
||||
var params = new AuthParams();
|
||||
params.setClientId(clientId);
|
||||
return params;
|
||||
}
|
||||
}
|
||||
12
src/main/java/com/ysoft/geecon/dto/DeviceResponse.java
Normal file
12
src/main/java/com/ysoft/geecon/dto/DeviceResponse.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.ysoft.geecon.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public record DeviceResponse(
|
||||
@JsonProperty("device_code") String deviceCode,
|
||||
@JsonProperty("user_code") String userCode,
|
||||
@JsonProperty("verification_uri") String verificationUri,
|
||||
@JsonProperty("interval") long interval,
|
||||
@JsonProperty("expires_in") long expiresIn
|
||||
) {
|
||||
}
|
||||
@@ -21,7 +21,7 @@ public class ClientsRepo {
|
||||
return Optional.ofNullable(clients.get(clientId));
|
||||
}
|
||||
|
||||
private void register(OAuthClient client) {
|
||||
public void register(OAuthClient client) {
|
||||
clients.put(client.clientId(), client);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,15 +12,16 @@ import java.util.*;
|
||||
public class SessionsRepo {
|
||||
private final Map<String, AuthorizationSession> authorizationSessions = new HashMap<>();
|
||||
private final Map<String, String> sessionsByAuthorizationCode = new HashMap<>();
|
||||
private final Map<String, String> sessionsByUserCode = 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;
|
||||
AuthorizationSession session = new AuthorizationSession(params, client);
|
||||
authorizationSessions.put(session.sessionId(), session);
|
||||
return session.sessionId();
|
||||
}
|
||||
|
||||
public AuthorizationSession authorizeSession(String sessionId, List<String> acceptedScopes) {
|
||||
@@ -39,9 +40,27 @@ public class SessionsRepo {
|
||||
return authCode;
|
||||
}
|
||||
|
||||
public String generateUserCode(String sessionId) {
|
||||
var authCode = SecureRandomStrings.alphanumeric(8);
|
||||
sessionsByUserCode.put(authCode, sessionId);
|
||||
return authCode;
|
||||
}
|
||||
|
||||
public Optional<AuthorizationSession> redeemAuthorizationCode(String authorizationCode) {
|
||||
var sessionId = Optional.ofNullable(sessionsByAuthorizationCode.get(authorizationCode));
|
||||
sessionId.ifPresent(_id -> sessionsByAuthorizationCode.remove(authorizationCode));
|
||||
return sessionId.map(authorizationSessions::get);
|
||||
}
|
||||
|
||||
public Optional<AuthorizationSession> redeemUserCode(String userCode) {
|
||||
var sessionId = Optional.ofNullable(sessionsByUserCode.get(userCode));
|
||||
sessionId.ifPresent(_id -> sessionsByUserCode.remove(userCode));
|
||||
return sessionId.map(authorizationSessions::get);
|
||||
}
|
||||
|
||||
|
||||
public Optional<AuthorizationSession> getByAuthorizationCode(String authorizationCode) {
|
||||
var sessionId = Optional.ofNullable(sessionsByAuthorizationCode.get(authorizationCode));
|
||||
return sessionId.map(authorizationSessions::get);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.ysoft.geecon.repo;
|
||||
|
||||
import com.ysoft.geecon.dto.OAuthClient;
|
||||
import com.ysoft.geecon.dto.User;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
@@ -20,7 +19,7 @@ public class UsersRepo {
|
||||
return Optional.ofNullable(users.get(username));
|
||||
}
|
||||
|
||||
private void register(User user) {
|
||||
public void register(User user) {
|
||||
users.put(user.login(), user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
|
||||
<p>You are authorizing to <strong>{client.description}</strong>, which requests the following permissions from you:</p>
|
||||
|
||||
<form action="" method="post">
|
||||
<form action="/auth/consent" method="post">
|
||||
<input type="hidden" name="sessionId" value="{sessionId}">
|
||||
<ul>
|
||||
{#each scopes}
|
||||
|
||||
85
src/main/resources/templates/OAuthResource/deviceLogin.html
Normal file
85
src/main/resources/templates/OAuthResource/deviceLogin.html
Normal file
@@ -0,0 +1,85 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login Page</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f4f4f4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login-container h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
background-color: #007BFF;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.error-popup {
|
||||
background-color: #ff6b6b;
|
||||
color: #fff;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<h2>Login</h2>
|
||||
{#if error == "invalid_code"}
|
||||
<div class="error-popup" id="error-popup">Invalid code</div>
|
||||
{/if}
|
||||
<form action="" method="post">
|
||||
<div class="form-group">
|
||||
<label for="code">User code:</label>
|
||||
<input type="text" id="code" name="code" required>
|
||||
</div>
|
||||
<button type="submit" class="login-button">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -73,7 +73,7 @@
|
||||
{#if error == "invalid_credentials"}
|
||||
<div class="error-popup" id="error-popup">Invalid credentials</div>
|
||||
{/if}
|
||||
<form action="" method="post">
|
||||
<form action="/auth" method="post">
|
||||
<input type="hidden" name="sessionId" value="{sessionId}">
|
||||
<div class="form-group">
|
||||
<label for="username">Username:</label>
|
||||
|
||||
76
src/main/resources/templates/OAuthResource/loginSuccess.html
Normal file
76
src/main/resources/templates/OAuthResource/loginSuccess.html
Normal file
@@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login Page</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f4f4f4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login-container h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
background-color: #007BFF;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.error-popup {
|
||||
background-color: #ff6b6b;
|
||||
color: #fff;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<h2>Successful login</h2>
|
||||
<p>You may close this window</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user