mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 05:01:07 +00:00
checkpoint on oauth for static site
- store state + redirectUri in a table - redirect again to get code & state out of query string - add meta-data validation to oauth2 module
This commit is contained in:
@ -646,6 +646,8 @@ public class QInstanceValidator
|
|||||||
validateSimpleCodeReference("Instance Authentication meta data customizer ", authentication.getCustomizer(), QAuthenticationModuleCustomizerInterface.class);
|
validateSimpleCodeReference("Instance Authentication meta data customizer ", authentication.getCustomizer(), QAuthenticationModuleCustomizerInterface.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authentication.validate(qInstance, this);
|
||||||
|
|
||||||
runPlugins(QAuthenticationMetaData.class, authentication, qInstance);
|
runPlugins(QAuthenticationMetaData.class, authentication, qInstance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,9 +23,12 @@ package com.kingsrook.qqq.backend.core.model.metadata.authentication;
|
|||||||
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
|
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
|
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
|
||||||
import com.kingsrook.qqq.backend.core.modules.authentication.implementations.OAuth2AuthenticationModule;
|
import com.kingsrook.qqq.backend.core.modules.authentication.implementations.OAuth2AuthenticationModule;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -37,6 +40,9 @@ public class OAuth2AuthenticationMetaData extends QAuthenticationMetaData
|
|||||||
private String tokenUrl;
|
private String tokenUrl;
|
||||||
private String clientId;
|
private String clientId;
|
||||||
|
|
||||||
|
private String userSessionTableName;
|
||||||
|
private String redirectStateTableName;
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// keep this secret, on the server - don't let it be serialized and sent to a client! //
|
// keep this secret, on the server - don't let it be serialized and sent to a client! //
|
||||||
////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -61,6 +67,33 @@ public class OAuth2AuthenticationMetaData extends QAuthenticationMetaData
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
@Override
|
||||||
|
public void validate(QInstance qInstance, QInstanceValidator qInstanceValidator)
|
||||||
|
{
|
||||||
|
super.validate(qInstance, qInstanceValidator);
|
||||||
|
|
||||||
|
String prefix = "OAuth2AuthenticationMetaData (named '" + getName() + "'): ";
|
||||||
|
|
||||||
|
qInstanceValidator.assertCondition(StringUtils.hasContent(baseUrl), prefix + "baseUrl must be set");
|
||||||
|
qInstanceValidator.assertCondition(StringUtils.hasContent(clientId), prefix + "clientId must be set");
|
||||||
|
qInstanceValidator.assertCondition(StringUtils.hasContent(clientSecret), prefix + "clientSecret must be set");
|
||||||
|
|
||||||
|
if(qInstanceValidator.assertCondition(StringUtils.hasContent(userSessionTableName), prefix + "userSessionTableName must be set"))
|
||||||
|
{
|
||||||
|
qInstanceValidator.assertCondition(qInstance.getTable(userSessionTableName) != null, prefix + "userSessionTableName ('" + userSessionTableName + "') was not found in the instance");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(qInstanceValidator.assertCondition(StringUtils.hasContent(redirectStateTableName), prefix + "redirectStateTableName must be set"))
|
||||||
|
{
|
||||||
|
qInstanceValidator.assertCondition(qInstance.getTable(redirectStateTableName) != null, prefix + "redirectStateTableName ('" + redirectStateTableName + "') was not found in the instance");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Fluent setter, override to help fluent flows
|
** Fluent setter, override to help fluent flows
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -189,4 +222,66 @@ public class OAuth2AuthenticationMetaData extends QAuthenticationMetaData
|
|||||||
return (this);
|
return (this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for userSessionTableName
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getUserSessionTableName()
|
||||||
|
{
|
||||||
|
return (this.userSessionTableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for userSessionTableName
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setUserSessionTableName(String userSessionTableName)
|
||||||
|
{
|
||||||
|
this.userSessionTableName = userSessionTableName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for userSessionTableName
|
||||||
|
*******************************************************************************/
|
||||||
|
public OAuth2AuthenticationMetaData withUserSessionTableName(String userSessionTableName)
|
||||||
|
{
|
||||||
|
this.userSessionTableName = userSessionTableName;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for redirectStateTableName
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getRedirectStateTableName()
|
||||||
|
{
|
||||||
|
return (this.redirectStateTableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for redirectStateTableName
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setRedirectStateTableName(String redirectStateTableName)
|
||||||
|
{
|
||||||
|
this.redirectStateTableName = redirectStateTableName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for redirectStateTableName
|
||||||
|
*******************************************************************************/
|
||||||
|
public OAuth2AuthenticationMetaData withRedirectStateTableName(String redirectStateTableName)
|
||||||
|
{
|
||||||
|
this.redirectStateTableName = redirectStateTableName;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.authentication;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import com.fasterxml.jackson.annotation.JsonFilter;
|
import com.fasterxml.jackson.annotation.JsonFilter;
|
||||||
|
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
|
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
|
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
|
||||||
@ -225,4 +226,15 @@ public class QAuthenticationMetaData implements TopLevelMetaDataInterface
|
|||||||
return (this);
|
return (this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
public void validate(QInstance qInstance, QInstanceValidator qInstanceValidator)
|
||||||
|
{
|
||||||
|
//////////////////
|
||||||
|
// noop at base //
|
||||||
|
//////////////////
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@ public interface QAuthenticationModuleInterface
|
|||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
default String getLoginRedirectUrl(String originalUrl)
|
default String getLoginRedirectUrl(String originalUrl) throws QAuthenticationException
|
||||||
{
|
{
|
||||||
throw (new NotImplementedException("The method getLoginRedirectUrl() is not implemented in the authentication module: " + this.getClass().getSimpleName()));
|
throw (new NotImplementedException("The method getLoginRedirectUrl() is not implemented in the authentication module: " + this.getClass().getSimpleName()));
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
package com.kingsrook.qqq.backend.core.modules.authentication.implementations;
|
package com.kingsrook.qqq.backend.core.modules.authentication.implementations;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
@ -33,7 +34,7 @@ import java.util.Base64;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import com.auth0.jwt.JWT;
|
import com.auth0.jwt.JWT;
|
||||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||||
@ -48,16 +49,20 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
|||||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.authentication.OAuth2AuthenticationMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.authentication.OAuth2AuthenticationMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||||
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
|
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
|
||||||
import com.kingsrook.qqq.backend.core.model.session.QUser;
|
import com.kingsrook.qqq.backend.core.model.session.QUser;
|
||||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
|
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
|
||||||
import com.kingsrook.qqq.backend.core.modules.authentication.implementations.model.UserSession;
|
import com.kingsrook.qqq.backend.core.modules.authentication.implementations.model.UserSession;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
|
import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
|
||||||
import com.nimbusds.oauth2.sdk.AuthorizationCode;
|
import com.nimbusds.oauth2.sdk.AuthorizationCode;
|
||||||
import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
|
import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
|
||||||
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
|
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
|
||||||
import com.nimbusds.oauth2.sdk.ErrorObject;
|
import com.nimbusds.oauth2.sdk.ErrorObject;
|
||||||
|
import com.nimbusds.oauth2.sdk.GeneralException;
|
||||||
|
import com.nimbusds.oauth2.sdk.ParseException;
|
||||||
import com.nimbusds.oauth2.sdk.Scope;
|
import com.nimbusds.oauth2.sdk.Scope;
|
||||||
import com.nimbusds.oauth2.sdk.TokenRequest;
|
import com.nimbusds.oauth2.sdk.TokenRequest;
|
||||||
import com.nimbusds.oauth2.sdk.TokenResponse;
|
import com.nimbusds.oauth2.sdk.TokenResponse;
|
||||||
@ -65,8 +70,11 @@ import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
|
|||||||
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
|
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
|
||||||
import com.nimbusds.oauth2.sdk.auth.Secret;
|
import com.nimbusds.oauth2.sdk.auth.Secret;
|
||||||
import com.nimbusds.oauth2.sdk.id.ClientID;
|
import com.nimbusds.oauth2.sdk.id.ClientID;
|
||||||
|
import com.nimbusds.oauth2.sdk.id.Issuer;
|
||||||
|
import com.nimbusds.oauth2.sdk.id.State;
|
||||||
import com.nimbusds.oauth2.sdk.pkce.CodeVerifier;
|
import com.nimbusds.oauth2.sdk.pkce.CodeVerifier;
|
||||||
import com.nimbusds.oauth2.sdk.token.AccessToken;
|
import com.nimbusds.oauth2.sdk.token.AccessToken;
|
||||||
|
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||||
|
|
||||||
@ -84,8 +92,8 @@ public class OAuth2AuthenticationModule implements QAuthenticationModuleInterfac
|
|||||||
.withTimeout(Duration.of(1, ChronoUnit.MINUTES))
|
.withTimeout(Duration.of(1, ChronoUnit.MINUTES))
|
||||||
.withMaxSize(1000);
|
.withMaxSize(1000);
|
||||||
|
|
||||||
// todo wip
|
private static final Memoization<String, OIDCProviderMetadata> oidcProviderMetadataMemoization = new Memoization<String, OIDCProviderMetadata>()
|
||||||
private static Map<String, String> stateToRedirectUrl = new HashMap<>();
|
.withMayStoreNullValues(false);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -101,36 +109,42 @@ public class OAuth2AuthenticationModule implements QAuthenticationModuleInterfac
|
|||||||
|
|
||||||
if(context.containsKey("code") && context.containsKey("state"))
|
if(context.containsKey("code") && context.containsKey("state"))
|
||||||
{
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////////
|
||||||
|
// handle a callback to initially auth a user for a traditional //
|
||||||
|
// (non-js) site - where the code & state params come to the backend //
|
||||||
|
///////////////////////////////////////////////////////////////////////
|
||||||
AuthorizationCode code = new AuthorizationCode(context.get("code"));
|
AuthorizationCode code = new AuthorizationCode(context.get("code"));
|
||||||
|
|
||||||
// todo - maybe this comes from lookup of state?
|
/////////////////////////////////////////
|
||||||
URI redirectURI = new URI(stateToRedirectUrl.get(context.get("state")));
|
// verify the state in our state table //
|
||||||
|
/////////////////////////////////////////
|
||||||
|
AtomicReference<String> redirectUri = new AtomicReference<>(null);
|
||||||
|
QContext.withTemporaryContext(new CapturedContext(qInstance, new QSystemUserSession()), () ->
|
||||||
|
{
|
||||||
|
QRecord redirectStateRecord = GetAction.execute(oauth2MetaData.getRedirectStateTableName(), Map.of("state", context.get("state")));
|
||||||
|
if(redirectStateRecord == null)
|
||||||
|
{
|
||||||
|
throw (new QAuthenticationException("State not found"));
|
||||||
|
}
|
||||||
|
redirectUri.set(redirectStateRecord.getValueString("redirectUri"));
|
||||||
|
});
|
||||||
|
|
||||||
|
URI redirectURI = new URI(redirectUri.get());
|
||||||
ClientSecretBasic clientSecretBasic = new ClientSecretBasic(new ClientID(oauth2MetaData.getClientId()), new Secret(oauth2MetaData.getClientSecret()));
|
ClientSecretBasic clientSecretBasic = new ClientSecretBasic(new ClientID(oauth2MetaData.getClientId()), new Secret(oauth2MetaData.getClientSecret()));
|
||||||
AuthorizationCodeGrant codeGrant = new AuthorizationCodeGrant(code, redirectURI);
|
AuthorizationCodeGrant codeGrant = new AuthorizationCodeGrant(code, redirectURI);
|
||||||
|
|
||||||
URI tokenEndpoint = new URI(oauth2MetaData.getTokenUrl());
|
URI tokenEndpoint = getOIDCProviderMetadata(oauth2MetaData).getTokenEndpointURI();
|
||||||
TokenRequest tokenRequest = new TokenRequest(tokenEndpoint, clientSecretBasic, codeGrant);
|
Scope scope = new Scope("openid profile email offline_access");
|
||||||
TokenResponse tokenResponse = TokenResponse.parse(tokenRequest.toHTTPRequest().send());
|
TokenRequest tokenRequest = new TokenRequest(tokenEndpoint, clientSecretBasic, codeGrant, scope);
|
||||||
|
|
||||||
if(tokenResponse.indicatesSuccess())
|
return createSessionFromTokenRequest(tokenRequest);
|
||||||
{
|
|
||||||
AccessToken accessToken = tokenResponse.toSuccessResponse().getTokens().getAccessToken();
|
|
||||||
// todo - what?? RefreshToken refreshToken = tokenResponse.toSuccessResponse().getTokens().getRefreshToken();
|
|
||||||
|
|
||||||
QSession session = createSessionFromToken(accessToken.getValue());
|
|
||||||
insertUserSession(accessToken.getValue(), session);
|
|
||||||
return (session);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ErrorObject errorObject = tokenResponse.toErrorResponse().getErrorObject();
|
|
||||||
LOG.info("Token request failed", logPair("code", errorObject.getCode()), logPair("description", errorObject.getDescription()));
|
|
||||||
throw (new QAuthenticationException(errorObject.getDescription()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if(context.containsKey("code") && context.containsKey("redirectUri") && context.containsKey("codeVerifier"))
|
else if(context.containsKey("code") && context.containsKey("redirectUri") && context.containsKey("codeVerifier"))
|
||||||
{
|
{
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// handle a call down to this backend code to initially auth a user for an //
|
||||||
|
// SPA that received a code (where the javascript generated the codeVerifier) //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
AuthorizationCode code = new AuthorizationCode(context.get("code"));
|
AuthorizationCode code = new AuthorizationCode(context.get("code"));
|
||||||
URI callback = new URI(context.get("redirectUri"));
|
URI callback = new URI(context.get("redirectUri"));
|
||||||
CodeVerifier codeVerifier = new CodeVerifier(context.get("codeVerifier"));
|
CodeVerifier codeVerifier = new CodeVerifier(context.get("codeVerifier"));
|
||||||
@ -140,30 +154,18 @@ public class OAuth2AuthenticationModule implements QAuthenticationModuleInterfac
|
|||||||
Secret clientSecret = new Secret(oauth2MetaData.getClientSecret());
|
Secret clientSecret = new Secret(oauth2MetaData.getClientSecret());
|
||||||
ClientAuthentication clientAuth = new ClientSecretBasic(clientID, clientSecret);
|
ClientAuthentication clientAuth = new ClientSecretBasic(clientID, clientSecret);
|
||||||
|
|
||||||
URI tokenEndpoint = new URI(oauth2MetaData.getTokenUrl());
|
URI tokenEndpoint = getOIDCProviderMetadata(oauth2MetaData).getTokenEndpointURI();
|
||||||
Scope scope = new Scope("openid profile email offline_access");
|
Scope scope = new Scope("openid profile email offline_access");
|
||||||
TokenRequest tokenRequest = new TokenRequest(tokenEndpoint, clientAuth, codeGrant, scope);
|
TokenRequest tokenRequest = new TokenRequest(tokenEndpoint, clientAuth, codeGrant, scope);
|
||||||
|
|
||||||
TokenResponse tokenResponse = TokenResponse.parse(tokenRequest.toHTTPRequest().send());
|
return createSessionFromTokenRequest(tokenRequest);
|
||||||
|
|
||||||
if(tokenResponse.indicatesSuccess())
|
|
||||||
{
|
|
||||||
AccessToken accessToken = tokenResponse.toSuccessResponse().getTokens().getAccessToken();
|
|
||||||
// todo - what?? RefreshToken refreshToken = tokenResponse.toSuccessResponse().getTokens().getRefreshToken();
|
|
||||||
|
|
||||||
QSession session = createSessionFromToken(accessToken.getValue());
|
|
||||||
insertUserSession(accessToken.getValue(), session);
|
|
||||||
return (session);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ErrorObject errorObject = tokenResponse.toErrorResponse().getErrorObject();
|
|
||||||
LOG.info("Token request failed", logPair("code", errorObject.getCode()), logPair("description", errorObject.getDescription()));
|
|
||||||
throw (new QAuthenticationException(errorObject.getDescription()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if(context.containsKey("sessionUUID") || context.containsKey("sessionId") || context.containsKey("uuid"))
|
else if(context.containsKey("sessionUUID") || context.containsKey("sessionId") || context.containsKey("uuid"))
|
||||||
{
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
// handle a "normal" request, where we aren't opening a new session //
|
||||||
|
// per-se, but instead are looking for one in our userSession table //
|
||||||
|
//////////////////////////////////////////////////////////////////////
|
||||||
String uuid = Objects.requireNonNullElseGet(context.get("sessionUUID"), () ->
|
String uuid = Objects.requireNonNullElseGet(context.get("sessionUUID"), () ->
|
||||||
Objects.requireNonNullElseGet(context.get("sessionId"), () ->
|
Objects.requireNonNullElseGet(context.get("sessionId"), () ->
|
||||||
context.get("uuid")));
|
context.get("uuid")));
|
||||||
@ -171,7 +173,11 @@ public class OAuth2AuthenticationModule implements QAuthenticationModuleInterfac
|
|||||||
String accessToken = getAccessTokenFromSessionUUID(uuid);
|
String accessToken = getAccessTokenFromSessionUUID(uuid);
|
||||||
QSession session = createSessionFromToken(accessToken);
|
QSession session = createSessionFromToken(accessToken);
|
||||||
session.setUuid(uuid);
|
session.setUuid(uuid);
|
||||||
// todo - validate its age or against provider??
|
|
||||||
|
//////////////////////////////////////////////////////////////////
|
||||||
|
// todo - do we need to validate its age or ping the provider?? //
|
||||||
|
//////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
return (session);
|
return (session);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -193,6 +199,36 @@ public class OAuth2AuthenticationModule implements QAuthenticationModuleInterfac
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
private QSession createSessionFromTokenRequest(TokenRequest tokenRequest) throws ParseException, IOException, QException
|
||||||
|
{
|
||||||
|
TokenResponse tokenResponse = TokenResponse.parse(tokenRequest.toHTTPRequest().send());
|
||||||
|
|
||||||
|
if(tokenResponse.indicatesSuccess())
|
||||||
|
{
|
||||||
|
AccessToken accessToken = tokenResponse.toSuccessResponse().getTokens().getAccessToken();
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////
|
||||||
|
// todo - do we want to try to do anything with a refresh token?? //
|
||||||
|
////////////////////////////////////////////////////////////////////
|
||||||
|
// RefreshToken refreshToken = tokenResponse.toSuccessResponse().getTokens().getRefreshToken();
|
||||||
|
|
||||||
|
QSession session = createSessionFromToken(accessToken.getValue());
|
||||||
|
insertUserSession(accessToken.getValue(), session);
|
||||||
|
return (session);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ErrorObject errorObject = tokenResponse.toErrorResponse().getErrorObject();
|
||||||
|
LOG.info("Token request failed", logPair("code", errorObject.getCode()), logPair("description", errorObject.getDescription()));
|
||||||
|
throw (new QAuthenticationException(errorObject.getDescription()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
@ -228,23 +264,54 @@ public class OAuth2AuthenticationModule implements QAuthenticationModuleInterfac
|
|||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
@Override
|
@Override
|
||||||
public String getLoginRedirectUrl(String originalUrl)
|
public String getLoginRedirectUrl(String originalUrl) throws QAuthenticationException
|
||||||
{
|
{
|
||||||
QInstance qInstance = QContext.getQInstance();
|
try
|
||||||
OAuth2AuthenticationMetaData oauth2MetaData = (OAuth2AuthenticationMetaData) qInstance.getAuthentication();
|
{
|
||||||
|
QInstance qInstance = QContext.getQInstance();
|
||||||
|
OAuth2AuthenticationMetaData oauth2MetaData = (OAuth2AuthenticationMetaData) qInstance.getAuthentication();
|
||||||
|
String authUrl = getOIDCProviderMetadata(oauth2MetaData).getAuthorizationEndpointURI().toString();
|
||||||
|
|
||||||
// todo wip - get from meta-data or from that thing that knows the other things?
|
QTableMetaData stateTable = QContext.getQInstance().getTable(oauth2MetaData.getRedirectStateTableName());
|
||||||
String authUrl = oauth2MetaData.getTokenUrl().replace("token", "authorize");
|
if(stateTable == null)
|
||||||
|
{
|
||||||
|
throw (new QAuthenticationException("The table specified as the oauthRedirectStateTableName [" + oauth2MetaData.getRedirectStateTableName() + "] is not defined in the QInstance"));
|
||||||
|
}
|
||||||
|
|
||||||
String state = UUID.randomUUID().toString();
|
///////////////////////////////////////////////////////////////////
|
||||||
stateToRedirectUrl.put(state, originalUrl);
|
// generate a secure state, of either default length (32 bytes), //
|
||||||
|
// or at a size (base64 encoded) that fits in the state table //
|
||||||
|
///////////////////////////////////////////////////////////////////
|
||||||
|
Integer stateStringLength = stateTable.getField("state").getMaxLength();
|
||||||
|
State state = stateStringLength == null ? new State(32) : new State((stateStringLength / 4) * 3);
|
||||||
|
String stateValue = state.getValue();
|
||||||
|
|
||||||
return authUrl +
|
/////////////////////////////
|
||||||
"?client_id=" + URLEncoder.encode(oauth2MetaData.getClientId(), StandardCharsets.UTF_8) +
|
// insert the state record //
|
||||||
"&redirect_uri=" + URLEncoder.encode(originalUrl, StandardCharsets.UTF_8) +
|
/////////////////////////////
|
||||||
"&response_type=code" +
|
QContext.withTemporaryContext(new CapturedContext(qInstance, new QSystemUserSession()), () ->
|
||||||
"&scope=" + URLEncoder.encode("openid profile email", StandardCharsets.UTF_8) +
|
{
|
||||||
"&state=" + URLEncoder.encode(state, StandardCharsets.UTF_8);
|
QRecord insertedState = new InsertAction().execute(new InsertInput(oauth2MetaData.getRedirectStateTableName()).withRecord(new QRecord()
|
||||||
|
.withValue("state", stateValue)
|
||||||
|
.withValue("redirectUri", originalUrl))).getRecords().get(0);
|
||||||
|
if(CollectionUtils.nullSafeHasContents(insertedState.getErrors()))
|
||||||
|
{
|
||||||
|
throw (new QAuthenticationException("Error storing redirect state: " + insertedState.getErrorsAsString()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return authUrl +
|
||||||
|
"?client_id=" + URLEncoder.encode(oauth2MetaData.getClientId(), StandardCharsets.UTF_8) +
|
||||||
|
"&redirect_uri=" + URLEncoder.encode(originalUrl, StandardCharsets.UTF_8) +
|
||||||
|
"&response_type=code" +
|
||||||
|
"&scope=" + URLEncoder.encode("openid profile email", StandardCharsets.UTF_8) +
|
||||||
|
"&state=" + URLEncoder.encode(state.getValue(), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
LOG.warn("Error getting login redirect url", e);
|
||||||
|
throw (new QAuthenticationException("Error getting login redirect url", e));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -401,4 +468,19 @@ public class OAuth2AuthenticationModule implements QAuthenticationModuleInterfac
|
|||||||
return (true);
|
return (true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
private OIDCProviderMetadata getOIDCProviderMetadata(OAuth2AuthenticationMetaData oAuth2AuthenticationMetaData) throws GeneralException, IOException
|
||||||
|
{
|
||||||
|
return oidcProviderMetadataMemoization.getResult(oAuth2AuthenticationMetaData.getName(), (name ->
|
||||||
|
{
|
||||||
|
Issuer issuer = new Issuer(oAuth2AuthenticationMetaData.getBaseUrl());
|
||||||
|
OIDCProviderMetadata metadata = OIDCProviderMetadata.resolve(issuer);
|
||||||
|
return (metadata);
|
||||||
|
})).orElseThrow(() -> new GeneralException("Could not resolve OIDCProviderMetadata for " + oAuth2AuthenticationMetaData.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.modules.authentication.implementations.metadata;
|
||||||
|
|
||||||
|
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Meta Data Producer for RedirectState table
|
||||||
|
*******************************************************************************/
|
||||||
|
public class RedirectStateMetaDataProducer extends MetaDataProducer<QTableMetaData>
|
||||||
|
{
|
||||||
|
public static final String TABLE_NAME = "redirectState";
|
||||||
|
|
||||||
|
private final String backendName;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Constructor
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public RedirectStateMetaDataProducer(String backendName)
|
||||||
|
{
|
||||||
|
this.backendName = backendName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public QTableMetaData produce(QInstance qInstance) throws QException
|
||||||
|
{
|
||||||
|
QTableMetaData tableMetaData = new QTableMetaData()
|
||||||
|
.withName(TABLE_NAME)
|
||||||
|
.withBackendName(backendName)
|
||||||
|
.withRecordLabelFormat("%s")
|
||||||
|
.withRecordLabelFields("state")
|
||||||
|
.withPrimaryKeyField("id")
|
||||||
|
.withUniqueKey(new UniqueKey("state"))
|
||||||
|
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
|
||||||
|
|
||||||
|
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
|
||||||
|
.withField(new QFieldMetaData("state", QFieldType.STRING).withIsEditable(false).withMaxLength(45).withBehavior(ValueTooLongBehavior.ERROR))
|
||||||
|
.withField(new QFieldMetaData("redirectUri", QFieldType.STRING).withIsEditable(false).withMaxLength(4096).withBehavior(ValueTooLongBehavior.ERROR))
|
||||||
|
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false));
|
||||||
|
|
||||||
|
return tableMetaData;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -137,6 +137,7 @@ import com.kingsrook.qqq.middleware.javalin.misc.DownloadFileSupplementalAction;
|
|||||||
import io.javalin.Javalin;
|
import io.javalin.Javalin;
|
||||||
import io.javalin.apibuilder.EndpointGroup;
|
import io.javalin.apibuilder.EndpointGroup;
|
||||||
import io.javalin.http.Context;
|
import io.javalin.http.Context;
|
||||||
|
import io.javalin.http.Cookie;
|
||||||
import io.javalin.http.UploadedFile;
|
import io.javalin.http.UploadedFile;
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.eclipse.jetty.http.HttpStatus;
|
import org.eclipse.jetty.http.HttpStatus;
|
||||||
@ -535,14 +536,24 @@ public class QJavalinImplementation
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
///////////////////////////////////////////////
|
||||||
|
// note: duplicated in ExecutorSessionUtils //
|
||||||
|
///////////////////////////////////////////////
|
||||||
Map<String, String> authenticationContext = new HashMap<>();
|
Map<String, String> authenticationContext = new HashMap<>();
|
||||||
|
|
||||||
String sessionIdCookieValue = context.cookie(SESSION_ID_COOKIE_NAME);
|
String sessionIdCookieValue = context.cookie(SESSION_ID_COOKIE_NAME);
|
||||||
String sessionUuidCookieValue = context.cookie(Auth0AuthenticationModule.SESSION_UUID_KEY);
|
String sessionUuidCookieValue = context.cookie(Auth0AuthenticationModule.SESSION_UUID_KEY);
|
||||||
String authorizationHeaderValue = context.header("Authorization");
|
String authorizationHeaderValue = context.header("Authorization");
|
||||||
String apiKeyHeaderValue = context.header("x-api-key");
|
String apiKeyHeaderValue = context.header("x-api-key");
|
||||||
|
String codeQueryParamValue = context.queryParam("code");
|
||||||
|
String stateQueryParamValue = context.queryParam("state");
|
||||||
|
|
||||||
if(StringUtils.hasContent(sessionIdCookieValue))
|
if(StringUtils.hasContent(codeQueryParamValue) && StringUtils.hasContent(stateQueryParamValue))
|
||||||
|
{
|
||||||
|
authenticationContext.put("code", codeQueryParamValue);
|
||||||
|
authenticationContext.put("state", stateQueryParamValue);
|
||||||
|
}
|
||||||
|
else if(StringUtils.hasContent(sessionIdCookieValue))
|
||||||
{
|
{
|
||||||
///////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////
|
||||||
// sessionId - maybe used by table-based auth module //
|
// sessionId - maybe used by table-based auth module //
|
||||||
|
@ -60,14 +60,24 @@ public class ExecutorSessionUtils
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
/////////////////////////////////////////////////
|
||||||
|
// note: duplicated in QJavalinImplementation //
|
||||||
|
/////////////////////////////////////////////////
|
||||||
Map<String, String> authenticationContext = new HashMap<>();
|
Map<String, String> authenticationContext = new HashMap<>();
|
||||||
|
|
||||||
String sessionIdCookieValue = context.cookie(SESSION_ID_COOKIE_NAME);
|
String sessionIdCookieValue = context.cookie(SESSION_ID_COOKIE_NAME);
|
||||||
String sessionUuidCookieValue = context.cookie(Auth0AuthenticationModule.SESSION_UUID_KEY);
|
String sessionUuidCookieValue = context.cookie(Auth0AuthenticationModule.SESSION_UUID_KEY);
|
||||||
String authorizationHeaderValue = context.header("Authorization");
|
String authorizationHeaderValue = context.header("Authorization");
|
||||||
String apiKeyHeaderValue = context.header("x-api-key");
|
String apiKeyHeaderValue = context.header("x-api-key");
|
||||||
|
String codeQueryParamValue = context.queryParam("code");
|
||||||
|
String stateQueryParamValue = context.queryParam("state");
|
||||||
|
|
||||||
if(StringUtils.hasContent(sessionIdCookieValue))
|
if(StringUtils.hasContent(codeQueryParamValue) && StringUtils.hasContent(stateQueryParamValue))
|
||||||
|
{
|
||||||
|
authenticationContext.put("code", codeQueryParamValue);
|
||||||
|
authenticationContext.put("state", stateQueryParamValue);
|
||||||
|
}
|
||||||
|
else if(StringUtils.hasContent(sessionIdCookieValue))
|
||||||
{
|
{
|
||||||
///////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////
|
||||||
// sessionId - maybe used by table-based auth module //
|
// sessionId - maybe used by table-based auth module //
|
||||||
|
@ -55,6 +55,23 @@ public class SimpleRouteAuthenticator implements RouteAuthenticatorInterface
|
|||||||
{
|
{
|
||||||
QSession qSession = QJavalinImplementation.setupSession(context, null);
|
QSession qSession = QJavalinImplementation.setupSession(context, null);
|
||||||
LOG.debug("Session has been activated", logPair("uuid", qSession.getUuid()));
|
LOG.debug("Session has been activated", logPair("uuid", qSession.getUuid()));
|
||||||
|
|
||||||
|
if(context.queryParamMap().containsKey("code") && context.queryParamMap().containsKey("state"))
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// if this request was a callback from oauth, with code & state params, //
|
||||||
|
// then redirect one last time removing those from the query string //
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
String redirectURL = context.fullUrl().replace("code=" + context.queryParam("code"), "")
|
||||||
|
.replace("state=" + context.queryParam("state"), "")
|
||||||
|
.replaceFirst("&+$", "")
|
||||||
|
.replaceFirst("\\?&", "?")
|
||||||
|
.replaceFirst("\\?$", "");
|
||||||
|
context.redirect(redirectURL);
|
||||||
|
LOG.debug("Redirecting request to remove code and state parameters");
|
||||||
|
return (false);
|
||||||
|
}
|
||||||
|
|
||||||
return (true);
|
return (true);
|
||||||
}
|
}
|
||||||
catch(QAuthenticationException e)
|
catch(QAuthenticationException e)
|
||||||
|
Reference in New Issue
Block a user