From c620cdfb2dfaa625d7a970b41a680baee2573624 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 10 Sep 2024 13:42:44 +0530 Subject: [PATCH] fix: introspect api --- src/main/java/io/supertokens/oauth/OAuth.java | 24 ++++ .../session/accessToken/AccessToken.java | 40 +++++++ .../io/supertokens/webserver/Webserver.java | 2 + .../webserver/api/oauth/OAuthTokenAPI.java | 1 - .../api/oauth/OAuthTokenIntrospectAPI.java | 107 ++++++++++++++++++ 5 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index ff58f8471..2fd4b3f04 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -23,6 +23,7 @@ import io.supertokens.Main; import io.supertokens.config.Config; +import io.supertokens.exceptions.TryRefreshTokenException; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlag; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; @@ -39,6 +40,7 @@ import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.oauth.OAuthStorage; import io.supertokens.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; +import io.supertokens.session.accessToken.AccessToken; import io.supertokens.session.jwt.JWT.JWTException; import io.supertokens.signingkeys.JWTSigningKey; import io.supertokens.signingkeys.SigningKeys; @@ -381,4 +383,26 @@ public int getValue() { return value; } } + + public static JsonObject introspectAccessToken(Main main, AppIdentifier appIdentifier, Storage storage, + String token) throws StorageQueryException, StorageTransactionLogicException, TenantOrAppNotFoundException, UnsupportedJWTSigningAlgorithmException { + try { + JsonObject payload = AccessToken.getPayloadFromAccessToken(appIdentifier, main, token); + if (payload.has("stt") && payload.get("stt").getAsInt() == SessionTokenType.ACCESS_TOKEN.value) { + payload.addProperty("active", true); + payload.addProperty("token_type", "Bearer"); + payload.addProperty("token_use", "access_token"); + + return payload; + } + // else fallback to active: false + + } catch (TryRefreshTokenException e) { + // fallback to active: false + } + + JsonObject result = new JsonObject(); + result.addProperty("active", false); + return result; + } } diff --git a/src/main/java/io/supertokens/session/accessToken/AccessToken.java b/src/main/java/io/supertokens/session/accessToken/AccessToken.java index 4e316ad28..0b841642c 100644 --- a/src/main/java/io/supertokens/session/accessToken/AccessToken.java +++ b/src/main/java/io/supertokens/session/accessToken/AccessToken.java @@ -56,6 +56,46 @@ public class AccessToken { + public static JsonObject getPayloadFromAccessToken(AppIdentifier appIdentifier, + @Nonnull Main main, @Nonnull String token) + throws TenantOrAppNotFoundException, TryRefreshTokenException, StorageQueryException, + UnsupportedJWTSigningAlgorithmException, StorageTransactionLogicException { + List keyInfoList = SigningKeys.getInstance(appIdentifier, main).getAllKeys(); + Exception error = null; + JWT.JWTInfo jwtInfo = null; + JWT.JWTPreParseInfo preParseJWTInfo = null; + try { + preParseJWTInfo = JWT.preParseJWTInfo(token); + } catch (JWTException e) { + // This basically should never happen, but it means, that the token structure is wrong, can't verify + throw new TryRefreshTokenException(e); + } + + for (JWTSigningKeyInfo keyInfo : keyInfoList) { + try { + jwtInfo = JWT.verifyJWTAndGetPayload(preParseJWTInfo, + ((JWTAsymmetricSigningKeyInfo) keyInfo).publicKey); + error = null; + break; + } catch (NoSuchAlgorithmException e) { + // This basically should never happen, but it means, that can't verify any tokens, no need to retry + throw new TryRefreshTokenException(e); + } catch (KeyException | JWTException e) { + error = e; + } + } + + if (jwtInfo == null) { + throw new TryRefreshTokenException(error); + } + + if (jwtInfo.payload.get("exp").getAsLong() * 1000 < System.currentTimeMillis()) { + throw new TryRefreshTokenException("Access token expired"); + } + + return jwtInfo.payload; + } + // TODO: device fingerprint - store hash of this in JWT. private static AccessTokenInfo getInfoFromAccessToken(AppIdentifier appIdentifier, diff --git a/src/main/java/io/supertokens/webserver/Webserver.java b/src/main/java/io/supertokens/webserver/Webserver.java index 89ead8fe1..da4c134aa 100644 --- a/src/main/java/io/supertokens/webserver/Webserver.java +++ b/src/main/java/io/supertokens/webserver/Webserver.java @@ -52,6 +52,7 @@ import io.supertokens.webserver.api.oauth.OAuthAcceptAuthLoginRequestAPI; import io.supertokens.webserver.api.oauth.OAuthAcceptAuthLogoutRequestAPI; import io.supertokens.webserver.api.oauth.OAuthTokenAPI; +import io.supertokens.webserver.api.oauth.OAuthTokenIntrospectAPI; import io.supertokens.webserver.api.oauth.RemoveOAuthClientAPI; import io.supertokens.webserver.api.passwordless.*; import io.supertokens.webserver.api.session.*; @@ -296,6 +297,7 @@ private void setupRoutes() { addAPI(new OAuthGetAuthLogoutRequestAPI(main)); addAPI(new OAuthAcceptAuthLogoutRequestAPI(main)); addAPI(new OAuthRejectAuthLogoutRequestAPI(main)); + addAPI(new OAuthTokenIntrospectAPI(main)); StandardContext context = tomcatReference.getContext(); Tomcat tomcat = tomcatReference.getTomcat(); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java index d4157a95f..60676fd94 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenAPI.java @@ -41,7 +41,6 @@ import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.util.HashMap; -import java.util.List; import java.util.Map; public class OAuthTokenAPI extends WebserverAPI { diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java new file mode 100644 index 000000000..77a54f46b --- /dev/null +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthTokenIntrospectAPI.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.webserver.api.oauth; + +import com.google.gson.*; +import io.supertokens.Main; +import io.supertokens.jwt.exceptions.UnsupportedJWTSigningAlgorithmException; +import io.supertokens.multitenancy.exception.BadPermissionException; +import io.supertokens.oauth.OAuth; +import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.Storage; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.webserver.InputParser; +import io.supertokens.webserver.WebserverAPI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class OAuthTokenIntrospectAPI extends WebserverAPI { + + public OAuthTokenIntrospectAPI(Main main) { + super(main, RECIPE_ID.OAUTH.toString()); + } + + @Override + public String getPath() { + return "/recipe/oauth/introspect"; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + String token = InputParser.parseStringOrThrowError(input, "token", false); + + if (token.startsWith("st_rt_")) { + String iss = InputParser.parseStringOrThrowError(input, "iss", false); + + try { + OAuthProxyHelper.proxyFormPOST( + main, req, resp, + getAppIdentifier(req), + enforcePublicTenantAndGetPublicTenantStorage(req), + "/admin/oauth2/introspect", + true, + false, + () -> { + Map formFields = new HashMap<>(); + for (Map.Entry entry : input.entrySet()) { + formFields.put(entry.getKey(), entry.getValue().getAsString()); + } + + return formFields; + }, + HashMap::new, + (statusCode, headers, rawBody, jsonBody) -> { + JsonObject jsonObject = jsonBody.getAsJsonObject(); + + jsonObject.addProperty("iss", iss); + if (jsonObject.has("ext")) { + JsonObject ext = jsonObject.get("ext").getAsJsonObject(); + for (Map.Entry entry : ext.entrySet()) { + jsonObject.add(entry.getKey(), entry.getValue()); + } + jsonObject.remove("ext"); + } + + jsonObject.addProperty("status", "OK"); + super.sendJsonResponse(200, jsonBody, resp); + } + ); + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException e) { + throw new ServletException(e); + } + } else { + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + JsonObject response = OAuth.introspectAccessToken(main, appIdentifier, storage, token); + super.sendJsonResponse(200, response, resp); + + } catch (IOException | TenantOrAppNotFoundException | BadPermissionException | StorageQueryException | StorageTransactionLogicException | UnsupportedJWTSigningAlgorithmException e) { + throw new ServletException(e); + } + } + } +}