DAG authorization pending - now correctly in JSON with standard code

This commit is contained in:
Dusan Jakub
2023-09-19 10:30:56 +02:00
parent 9a7a437153
commit d6bd44e799
6 changed files with 31 additions and 24 deletions

View File

@@ -1,6 +1,7 @@
package com.ysoft.geecon; package com.ysoft.geecon;
import com.ysoft.geecon.dto.*; import com.ysoft.geecon.dto.*;
import com.ysoft.geecon.error.ErrorResponse;
import com.ysoft.geecon.error.OAuthException; import com.ysoft.geecon.error.OAuthException;
import com.ysoft.geecon.repo.ClientsRepo; import com.ysoft.geecon.repo.ClientsRepo;
import com.ysoft.geecon.repo.SessionsRepo; import com.ysoft.geecon.repo.SessionsRepo;
@@ -121,7 +122,7 @@ public class OAuthResource {
return switch (params.getGrantType()) { return switch (params.getGrantType()) {
case "authorization_code" -> redeemAuthorizationCode(params); case "authorization_code" -> redeemAuthorizationCode(params);
case "urn:ietf:params:oauth:grant-type:device_code" -> redeemDeviceCode(params); case "urn:ietf:params:oauth:grant-type:device_code" -> redeemDeviceCode(params);
default -> throw new OAuthException("Unsupported grant type"); default -> throw new OAuthException(ErrorResponse.Error.invalid_request, "Unsupported grant type");
}; };
} }
@@ -149,23 +150,23 @@ public class OAuthResource {
private AccessTokenResponse redeemAuthorizationCode(TokenParams params) { private AccessTokenResponse redeemAuthorizationCode(TokenParams params) {
var session = sessionsRepo.redeemAuthorizationCode(params.getCode()) var session = sessionsRepo.redeemAuthorizationCode(params.getCode())
.orElseThrow(() -> new OAuthException("Invalid code")); .orElseThrow(() -> new OAuthException(ErrorResponse.Error.access_denied, "Invalid code"));
validateClient(params, session); validateClient(params, session);
if (!session.validateCodeChallenge(params.getCodeVerifier())) { if (!session.validateCodeChallenge(params.getCodeVerifier())) {
throw new OAuthException("Invalid code verifier"); throw new OAuthException(ErrorResponse.Error.access_denied, "Invalid code verifier");
} }
return session.tokens(); return session.tokens();
} }
private AccessTokenResponse redeemDeviceCode(TokenParams params) { private AccessTokenResponse redeemDeviceCode(TokenParams params) {
var session = sessionsRepo.getByAuthorizationCode(params.getDeviceCode()) var session = sessionsRepo.getByAuthorizationCode(params.getDeviceCode())
.orElseThrow(() -> new OAuthException("Invalid code")); .orElseThrow(() -> new OAuthException(ErrorResponse.Error.access_denied, "Invalid device code"));
validateClient(params, session); validateClient(params, session);
if (session.tokens() != null) { if (session.tokens() != null) {
sessionsRepo.redeemAuthorizationCode(params.getDeviceCode()); sessionsRepo.redeemAuthorizationCode(params.getDeviceCode());
return session.tokens(); return session.tokens();
} else { } else {
throw new OAuthException("Authorization pending"); throw new OAuthException(ErrorResponse.Error.authorization_pending, "Authorization pending");
} }
} }
@@ -177,24 +178,24 @@ public class OAuthResource {
private OAuthClient validateClient(AuthParams params) { private OAuthClient validateClient(AuthParams params) {
var client = clientsRepo.getClient(params.getClientId()) var client = clientsRepo.getClient(params.getClientId())
.orElseThrow(() -> new RuntimeException("Not a valid client")); .orElseThrow(() -> new OAuthException(ErrorResponse.Error.invalid_request, "Not a valid client"));
if (!client.validateRedirectUri(params.getRedirectUri())) { if (!client.validateRedirectUri(params.getRedirectUri())) {
throw new RuntimeException("Invalid redirect URI"); throw new OAuthException(ErrorResponse.Error.invalid_request, "Invalid redirect URI");
} }
if (StringUtil.isNullOrEmpty(params.getState())) { if (StringUtil.isNullOrEmpty(params.getState())) {
throw new RuntimeException("Invalid state"); throw new OAuthException(ErrorResponse.Error.invalid_request, "Invalid state");
} }
return client; return client;
} }
private OAuthClient validateClient(TokenParams params, AuthorizationSession session) { private OAuthClient validateClient(TokenParams params, AuthorizationSession session) {
var client = clientsRepo.getClient(params.getClientId()) var client = clientsRepo.getClient(params.getClientId())
.orElseThrow(() -> new RuntimeException("Not a valid client")); .orElseThrow(() -> new OAuthException(ErrorResponse.Error.invalid_request, "Not a valid client"));
if (!session.validateRedirectUri(params.getRedirectUri())) { if (!session.validateRedirectUri(params.getRedirectUri())) {
throw new RuntimeException("Invalid redirect URI"); throw new OAuthException(ErrorResponse.Error.invalid_request, "Invalid redirect URI");
} }
if (!client.validateSecret(params.getClientSecret())) { if (!client.validateSecret(params.getClientSecret())) {
throw new RuntimeException("Invalid secret"); throw new OAuthException(ErrorResponse.Error.unauthorized_client, "Invalid secret");
} }
return client; return client;
} }

View File

@@ -0,0 +1,10 @@
package com.ysoft.geecon.error;
import com.fasterxml.jackson.annotation.JsonProperty;
public record ErrorResponse(@JsonProperty("error") Error error,
@JsonProperty("error_description") String description) {
public enum Error {
invalid_request, unauthorized_client, access_denied, invalid_scope, server_error, temporarily_unavailable, authorization_pending
}
}

View File

@@ -35,6 +35,6 @@ class ExceptionMappers {
@CheckedTemplate @CheckedTemplate
public static class Templates { public static class Templates {
public static native TemplateInstance error(OAuthException.ErrorResponse response); public static native TemplateInstance error(ErrorResponse response);
} }
} }

View File

@@ -1,30 +1,24 @@
package com.ysoft.geecon.error; package com.ysoft.geecon.error;
import com.fasterxml.jackson.annotation.JsonProperty;
public class OAuthException extends RuntimeException { public class OAuthException extends RuntimeException {
private final ErrorResponse response; private final ErrorResponse response;
// https://www.ietf.org/archive/id/draft-ietf-oauth-v2-1-09.html#name-error-response-2
public OAuthException(ErrorResponse response) { public OAuthException(ErrorResponse response) {
super("OAuth error: " + response.error() + " " + response.description()); super("OAuth error: " + response.error() + " " + response.description());
this.response = response; this.response = response;
} }
public OAuthException(String error, String description) { public OAuthException(ErrorResponse.Error error, String description) {
this(new ErrorResponse(error, description)); this(new ErrorResponse(error, description));
} }
@Deprecated @Deprecated
public OAuthException(String message) { public OAuthException(String message) {
this(message, message); this(ErrorResponse.Error.server_error, message);
} }
public ErrorResponse getResponse() { public ErrorResponse getResponse() {
return response; return response;
} }
public record ErrorResponse(@JsonProperty("error") String error,
@JsonProperty("error_description") String description) {
}
} }

View File

@@ -3,6 +3,7 @@ package com.ysoft.geecon;
import com.ysoft.geecon.dto.DeviceResponse; import com.ysoft.geecon.dto.DeviceResponse;
import com.ysoft.geecon.dto.OAuthClient; import com.ysoft.geecon.dto.OAuthClient;
import com.ysoft.geecon.dto.User; import com.ysoft.geecon.dto.User;
import com.ysoft.geecon.error.ErrorResponse;
import com.ysoft.geecon.helpers.ConsentScreen; import com.ysoft.geecon.helpers.ConsentScreen;
import com.ysoft.geecon.helpers.DeviceAuthorizationGrantFlow; import com.ysoft.geecon.helpers.DeviceAuthorizationGrantFlow;
import com.ysoft.geecon.helpers.DeviceCodeScreen; import com.ysoft.geecon.helpers.DeviceCodeScreen;
@@ -71,6 +72,7 @@ public class DeviceAuthGrantTest {
public void deviceAuthGrant_authorizationPending() throws IOException { public void deviceAuthGrant_authorizationPending() throws IOException {
DeviceAuthorizationGrantFlow flow = new DeviceAuthorizationGrantFlow(deviceUri, CLIENT); DeviceAuthorizationGrantFlow flow = new DeviceAuthorizationGrantFlow(deviceUri, CLIENT);
flow.start(); flow.start();
flow.exchangeDeviceCodeError(); ErrorResponse errorResponse = flow.exchangeDeviceCodeError();
assertThat(errorResponse.error(), is(ErrorResponse.Error.authorization_pending));
} }
} }

View File

@@ -3,7 +3,7 @@ package com.ysoft.geecon.helpers;
import com.ysoft.geecon.dto.AccessTokenResponse; import com.ysoft.geecon.dto.AccessTokenResponse;
import com.ysoft.geecon.dto.DeviceResponse; import com.ysoft.geecon.dto.DeviceResponse;
import com.ysoft.geecon.dto.OAuthClient; import com.ysoft.geecon.dto.OAuthClient;
import com.ysoft.geecon.error.OAuthException; import com.ysoft.geecon.error.ErrorResponse;
import java.io.IOException; import java.io.IOException;
@@ -56,7 +56,7 @@ public class DeviceAuthorizationGrantFlow {
.extract().as(AccessTokenResponse.class); .extract().as(AccessTokenResponse.class);
} }
public OAuthException.ErrorResponse exchangeDeviceCodeError() { public ErrorResponse exchangeDeviceCodeError() {
return given() return given()
.formParam("grant_type", "urn:ietf:params:oauth:grant-type:device_code") .formParam("grant_type", "urn:ietf:params:oauth:grant-type:device_code")
.formParam("client_id", client.clientId()) .formParam("client_id", client.clientId())
@@ -68,6 +68,6 @@ public class DeviceAuthorizationGrantFlow {
.contentType(JSON) .contentType(JSON)
.body("error", is(notNullValue())) .body("error", is(notNullValue()))
.body("error_description", is(notNullValue())) .body("error_description", is(notNullValue()))
.extract().as(OAuthException.ErrorResponse.class); .extract().as(ErrorResponse.class);
} }
} }