diff --git a/src/restless/main/java/org/comroid/restless/MimeType.java b/src/restless/main/java/org/comroid/restless/MimeType.java index 26d7b953..18ecbfc4 100644 --- a/src/restless/main/java/org/comroid/restless/MimeType.java +++ b/src/restless/main/java/org/comroid/restless/MimeType.java @@ -7,6 +7,7 @@ public final class MimeType implements Named, CharSequence { private static final Map cache = new ConcurrentHashMap<>(); + public static final MimeType ANY = forName("*/*"); public static final MimeType JSON = forName("application/json"); public static final MimeType XML = forName("application/xml"); public static final MimeType JAVASCRIPT = forName("application/javascript"); diff --git a/src/webkit/main/java/org/comroid/restless/server/RestServer.java b/src/webkit/main/java/org/comroid/restless/server/RestServer.java index c84f26a4..420ae373 100644 --- a/src/webkit/main/java/org/comroid/restless/server/RestServer.java +++ b/src/webkit/main/java/org/comroid/restless/server/RestServer.java @@ -13,6 +13,7 @@ import org.comroid.mutatio.model.Ref; import org.comroid.mutatio.ref.Reference; import org.comroid.restless.HTTPStatusCodes; +import org.comroid.restless.MimeType; import org.comroid.restless.REST; import org.comroid.restless.REST.Response; import org.comroid.restless.body.URIQueryEditor; @@ -165,12 +166,13 @@ public void handle(HttpExchange exchange) { throw new RestEndpointException(UNSUPPORTED_MEDIA_TYPE, "Unsupported Content-Type: " + contentType); logger.debug("Receiving {} {}-Request to {} with {} headers", serializer.getMimeType(), requestMethod, requestURI, requestHeaders.size()); - logger.trace("Response has headers:\n{}", requestHeaders.stream() + logger.trace("Request has headers:\n{}", requestHeaders.stream() .map(REST.Header::toString) .collect(Collectors.joining("\n"))); // get request body String body = consumeBody(exchange); + logger.trace("Request body: {}", body); UniNode requestData = null; try { requestData = body.isEmpty() ? serializer.createObjectNode() : serializer.parse(body); @@ -209,20 +211,26 @@ public void handle(HttpExchange exchange) { // execute endpoint logger.debug("Executing Endpoint {}...", endpoint); response = endpoint.executeMethod(context, requestMethod, requestHeaders, urlParams, requestData); - } catch (RestEndpointException e) { - logger.warn("A REST Endpoint exception was thrown: {}", e.getMessage()); - try { - Response alternate = tryRecoverFrom(e, requestURI, INTERNAL_SERVER_ERROR, requestMethod, requestHeaders); + } catch (Throwable t) { + if (t instanceof RestEndpointException && requestHeaders.getHeader(ACCEPTED_CONTENT_TYPE) + .getValues() + .stream() + .anyMatch(str -> str.contains(MimeType.HTML.toString()) || str.contains(MimeType.ANY.toString()))) { + RestEndpointException e = (RestEndpointException) t; + logger.warn("A REST Endpoint exception was thrown: {}", e.getMessage()); + try { + Response alternate = tryRecoverFrom(e, requestURI, INTERNAL_SERVER_ERROR, requestMethod, requestHeaders); - if (alternate != null && alternate.getStatusCode() == OK && e.getStatusCode() != OK) - response = alternate; - } catch (Throwable t2) { - logger.debug("An error occurred during recovery", t2); + if (alternate != null && alternate.getStatusCode() == OK && e.getStatusCode() != OK) + response = alternate; + } catch (Throwable t2) { + logger.debug("An error occurred during recovery", t2); + } + } else { + logger.error("An error occurred during request handling", t); + RestEndpointException wrapped = new RestEndpointException(INTERNAL_SERVER_ERROR, t); + response = new Response(wrapped.getStatusCode(), generateErrorNode(this, contentType, wrapped)); } - } catch (Throwable t) { - logger.error("An error occurred during request handling", t); - RestEndpointException wrapped = new RestEndpointException(INTERNAL_SERVER_ERROR, t); - response = new Response(wrapped.getStatusCode(), generateErrorNode(this, contentType, wrapped)); } // if response is null, send empty OK if (response == null) diff --git a/src/webkit/oauth/java/org/comroid/webkit/oauth/client/ClientProvider.java b/src/webkit/oauth/java/org/comroid/webkit/oauth/client/ClientProvider.java index 7e1d79df..9649b0c6 100644 --- a/src/webkit/oauth/java/org/comroid/webkit/oauth/client/ClientProvider.java +++ b/src/webkit/oauth/java/org/comroid/webkit/oauth/client/ClientProvider.java @@ -2,9 +2,11 @@ import org.comroid.api.Rewrapper; import org.comroid.restless.CommonHeaderNames; -import org.comroid.webkit.oauth.user.OAuthAuthorization; import org.comroid.restless.REST; import org.comroid.restless.server.RestEndpointException; +import org.comroid.util.Pair; +import org.comroid.webkit.oauth.model.ValidityStage; +import org.comroid.webkit.oauth.user.OAuthAuthorization; import java.util.UUID; @@ -19,7 +21,11 @@ default OAuthAuthorization.AccessToken findAccessToken(REST.Header.List headers) boolean hasClient(UUID uuid); + Rewrapper findClient(REST.Header.List headers); + Rewrapper findClient(UUID uuid); - Client loginClient(String email, String login); + Pair loginClient(String email, String login); + + ValidityStage findValidityStage(String token); } diff --git a/src/webkit/oauth/java/org/comroid/webkit/oauth/rest/OAuthEndpoint.java b/src/webkit/oauth/java/org/comroid/webkit/oauth/rest/OAuthEndpoint.java index 836a04db..66f7e2d3 100644 --- a/src/webkit/oauth/java/org/comroid/webkit/oauth/rest/OAuthEndpoint.java +++ b/src/webkit/oauth/java/org/comroid/webkit/oauth/rest/OAuthEndpoint.java @@ -3,15 +3,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.comroid.api.StreamSupplier; -import org.comroid.webkit.oauth.OAuth; -import org.comroid.webkit.oauth.client.Client; -import org.comroid.webkit.oauth.client.ClientProvider; -import org.comroid.webkit.oauth.model.OAuthError; -import org.comroid.webkit.oauth.resource.Resource; -import org.comroid.webkit.oauth.resource.ResourceProvider; -import org.comroid.webkit.oauth.rest.request.AuthenticationRequest; -import org.comroid.webkit.oauth.rest.request.TokenRequest; -import org.comroid.webkit.oauth.user.OAuthAuthorization; import org.comroid.restless.CommonHeaderNames; import org.comroid.restless.HTTPStatusCodes; import org.comroid.restless.REST; @@ -20,8 +11,20 @@ import org.comroid.restless.server.ServerEndpoint; import org.comroid.uniform.Context; import org.comroid.uniform.node.UniNode; +import org.comroid.util.Pair; import org.comroid.webkit.frame.FrameBuilder; import org.comroid.webkit.model.PagePropertiesProvider; +import org.comroid.webkit.oauth.OAuth; +import org.comroid.webkit.oauth.client.Client; +import org.comroid.webkit.oauth.client.ClientProvider; +import org.comroid.webkit.oauth.model.OAuthError; +import org.comroid.webkit.oauth.model.ValidityStage; +import org.comroid.webkit.oauth.resource.Resource; +import org.comroid.webkit.oauth.resource.ResourceProvider; +import org.comroid.webkit.oauth.rest.request.AuthenticationRequest; +import org.comroid.webkit.oauth.rest.request.TokenRequest; +import org.comroid.webkit.oauth.rest.request.TokenRevocationRequest; +import org.comroid.webkit.oauth.user.OAuthAuthorization; import org.intellij.lang.annotations.Language; import java.net.URI; @@ -58,13 +61,13 @@ public REST.Response executeGET(Context context, REST.Header.List headers, Strin final String userAgent = headers.getFirst(CommonHeaderNames.USER_AGENT); try { - // find session & account - Client account = context.requireFromContext(ClientProvider.class) - .findAccessToken(headers) - .getAuthorization() - .getClient(); + // find client + Client client = context.requireFromContext(ClientProvider.class) + .findClient(headers) + // throw with status code OK to send login frame + .orElseThrow(() -> new RestEndpointException(OK)); - String authorizationCode = completeAuthorization(account, authenticationRequest, context, service, userAgent); + String authorizationCode = completeAuthorization(client, authenticationRequest, context, service, userAgent); // assemble redirect uri query.put("code", authorizationCode); @@ -110,7 +113,7 @@ public REST.Response executePOST(Context context, REST.Header.List headers, Stri AuthenticationRequest authenticationRequest = loginRequests.getOrDefault(requestId, null); URIQueryEditor query = new URIQueryEditor(authenticationRequest.getRedirectURI()); - Client client; + Pair client; try { client = context.requireFromContext(ClientProvider.class) .loginClient(email, login); @@ -125,14 +128,18 @@ public REST.Response executePOST(Context context, REST.Header.List headers, Stri .orElseThrow(() -> new RestEndpointException(UNAUTHORIZED, "Service with ID " + clientID + " not found")); String userAgent = headers.getFirst(CommonHeaderNames.USER_AGENT); - String code = OAuthEndpoint.completeAuthorization(client, authenticationRequest, context, service, userAgent); + String code = OAuthEndpoint.completeAuthorization(client.getFirst(), authenticationRequest, context, service, userAgent); // assemble redirect uri query.put("code", code); if (authenticationRequest.state.isNonNull()) query.put("state", authenticationRequest.getState()); - return new REST.Response(FOUND, query.toURI()); + REST.Header.List response = new REST.Header.List(); + response.add("Location", query.toURI().toString()); + response.add("Set-Cookie", client.getSecond()); + + return new REST.Response(FOUND, response); } }, TOKEN("/token") { @@ -146,6 +153,34 @@ public REST.Response executePOST(Context context, REST.Header.List headers, Stri return new REST.Response(OK, accessToken); } }, + TOKEN_REVOKE("/token/revoke") { + @Override + public REST.Response executePOST(Context context, REST.Header.List headers, String[] urlParams, UniNode body) throws RestEndpointException { + ClientProvider clientProvider = context.requireFromContext(ClientProvider.class); + TokenRevocationRequest request = new TokenRevocationRequest(context, body); + + ValidityStage validity; + if (request.tokenHint.isNull()) { + validity = clientProvider.findValidityStage(request.getToken()); + } else switch (request.getTokenHint()) { + case "access_token": + validity = clientProvider.findAccessToken(request.getToken()); + break; + case "refresh_token": + // fixme + //validity = clientProvider.findAccessToken(request.getToken()); + throw new UnsupportedOperationException("unsupported: refresh token"); + default: + throw new AssertionError("invalid token hint: " + request.getTokenHint()); + } + + if (validity == null) + throw new RestEndpointException(BAD_REQUEST, "Unknown Token"); + if (validity.isValid() && !validity.invalidate()) + throw new RestEndpointException(INTERNAL_SERVER_ERROR, "Could not invalidate token"); + return new REST.Response(OK); + } + }, USER_INFO("/userInfo") { @Override public REST.Response executeGET(Context context, REST.Header.List headers, String[] urlParams, UniNode body) throws RestEndpointException { diff --git a/src/webkit/oauth/java/org/comroid/webkit/oauth/rest/request/AuthenticationRequest.java b/src/webkit/oauth/java/org/comroid/webkit/oauth/rest/request/AuthenticationRequest.java index cefffff6..9c4093c1 100644 --- a/src/webkit/oauth/java/org/comroid/webkit/oauth/rest/request/AuthenticationRequest.java +++ b/src/webkit/oauth/java/org/comroid/webkit/oauth/rest/request/AuthenticationRequest.java @@ -20,7 +20,7 @@ import java.util.stream.Stream; public class AuthenticationRequest extends DataContainerBase { - public static final String SCOPE_SPLIT_PATTERN = "[\\s+]"; + public static final String SCOPE_SPLIT_PATTERN = "[\\s+,]{1,2}"; @RootBind public static final GroupBind Type = new GroupBind<>(OAuth.CONTEXT, "authentication-request"); diff --git a/src/webkit/oauth/java/org/comroid/webkit/oauth/rest/request/TokenRevocationRequest.java b/src/webkit/oauth/java/org/comroid/webkit/oauth/rest/request/TokenRevocationRequest.java new file mode 100644 index 00000000..58995ee7 --- /dev/null +++ b/src/webkit/oauth/java/org/comroid/webkit/oauth/rest/request/TokenRevocationRequest.java @@ -0,0 +1,44 @@ +package org.comroid.webkit.oauth.rest.request; + +import org.comroid.mutatio.model.Ref; +import org.comroid.uniform.Context; +import org.comroid.uniform.node.UniNode; +import org.comroid.uniform.node.UniObjectNode; +import org.comroid.util.StandardValueType; +import org.comroid.varbind.annotation.RootBind; +import org.comroid.varbind.bind.GroupBind; +import org.comroid.varbind.bind.VarBind; +import org.comroid.varbind.container.DataContainerBase; +import org.comroid.webkit.oauth.OAuth; +import org.jetbrains.annotations.Nullable; + +public class TokenRevocationRequest extends DataContainerBase { + @RootBind + public static final GroupBind Type + = new GroupBind<>(OAuth.CONTEXT, "token-revocation-request"); + public static final VarBind TOKEN + = Type.createBind("token") + .extractAs(StandardValueType.STRING) + .asIdentities() + .onceEach() + .setRequired() + .build(); + public static final VarBind TOKEN_HINT + = Type.createBind("token_type_hint") + .extractAs(StandardValueType.STRING) + .build(); + public final Ref token = getComputedReference(TOKEN); + public final Ref tokenHint = getComputedReference(TOKEN_HINT); + + public String getToken() { + return token.assertion("token"); + } + + public @Nullable String getTokenHint() { + return tokenHint.get(); + } + + public TokenRevocationRequest(Context context, UniNode body) { + super(context, body.asObjectNode()); + } +} diff --git a/src/webkit/oauth/java/org/comroid/webkit/oauth/user/OAuthAuthorization.java b/src/webkit/oauth/java/org/comroid/webkit/oauth/user/OAuthAuthorization.java index 4175c059..be426433 100644 --- a/src/webkit/oauth/java/org/comroid/webkit/oauth/user/OAuthAuthorization.java +++ b/src/webkit/oauth/java/org/comroid/webkit/oauth/user/OAuthAuthorization.java @@ -3,6 +3,13 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.comroid.mutatio.model.Ref; +import org.comroid.uniform.Context; +import org.comroid.uniform.node.UniNode; +import org.comroid.util.StandardValueType; +import org.comroid.varbind.annotation.RootBind; +import org.comroid.varbind.bind.GroupBind; +import org.comroid.varbind.bind.VarBind; +import org.comroid.varbind.container.DataContainerBase; import org.comroid.webkit.oauth.OAuth; import org.comroid.webkit.oauth.client.Client; import org.comroid.webkit.oauth.client.ClientProvider; @@ -11,13 +18,6 @@ import org.comroid.webkit.oauth.resource.Resource; import org.comroid.webkit.oauth.resource.ResourceProvider; import org.comroid.webkit.oauth.rest.request.AuthenticationRequest; -import org.comroid.uniform.Context; -import org.comroid.uniform.node.UniNode; -import org.comroid.util.StandardValueType; -import org.comroid.varbind.annotation.RootBind; -import org.comroid.varbind.bind.GroupBind; -import org.comroid.varbind.bind.VarBind; -import org.comroid.varbind.container.DataContainerBase; import org.jetbrains.annotations.Nullable; import java.time.Duration; @@ -227,8 +227,12 @@ public boolean invalidate() { return invalidation.complete(null); } - public boolean checkToken(String token) { - return this.token.contentEquals(token); + public boolean checkToken(String otherTk) { + // strip prefix if present + int indexOfSpace = otherTk.indexOf(' '); + otherTk = indexOfSpace < 8 ? otherTk.substring(indexOfSpace + 1) : otherTk; + //logger.trace("checking token [{}] vs other token [{}]", this.token.get(), otherTk); + return this.token.contentEquals(otherTk); } } }