Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add webauthn support #83

Merged
merged 1 commit into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/main/java/com/descope/literals/Routes.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ public static class AuthEndPoints {
public static final String REPLACE_USER_PASSWORD_LINK = "/v1/auth/password/replace";
public static final String PASSWORD_POLICY_LINK = "/v1/auth/password/policy";

// WebAuthn
public static final String WEBAUTHN_SIGN_UP_START = "/v1/auth/webauthn/signup/start";
public static final String WEBAUTHN_SIGN_UP_FINISH = "/v1/auth/webauthn/signup/finish";
public static final String WEBAUTHN_SIGN_IN_START = "/v1/auth/webauthn/signin/start";
public static final String WEBAUTHN_SIGN_IN_FINISH = "/v1/auth/webauthn/signin/finish";
public static final String WEBAUTHN_SIGN_UP_OR_IN_START = "/v1/auth/webauthn/signup-in/start";
public static final String WEBAUTHN_UPDATE_START = "/v1/auth/webauthn/update/start";
public static final String WEBAUTHN_UPDATE_FINISH = "/v1/auth/webauthn/update/finish";

public static final String GET_KEYS_LINK = "/v2/keys";
public static final String REFRESH_TOKEN_LINK = "/v1/auth/refresh";
public static final String EXCHANGE_ACCESS_KEY_LINK = "/v1/auth/accesskey/exchange";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.descope.sdk.auth.PasswordService;
import com.descope.sdk.auth.SAMLService;
import com.descope.sdk.auth.TOTPService;
import com.descope.sdk.auth.WebAuthnService;
import lombok.Builder;
import lombok.Getter;

Expand All @@ -22,4 +23,5 @@ public class AuthenticationServices {
PasswordService passwordService;
MagicLinkService magicLinkService;
EnchantedLinkService enchantedLinkService;
WebAuthnService webAuthnService;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.descope.model.webauthn;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class WebAuthnFinishRequest {
private String transactionId;
private String response;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.descope.model.webauthn;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class WebAuthnTransactionResponse {
private String transactionId;
private String options;
private boolean create;
}
97 changes: 97 additions & 0 deletions src/main/java/com/descope/sdk/auth/WebAuthnService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.descope.sdk.auth;

import com.descope.exception.DescopeException;
import com.descope.model.auth.AuthenticationInfo;
import com.descope.model.magiclink.LoginOptions;
import com.descope.model.user.User;
import com.descope.model.webauthn.WebAuthnFinishRequest;
import com.descope.model.webauthn.WebAuthnTransactionResponse;

/**
* Implements WebAuthn server side authentication as a mid-layer between client WebAuthn and Descope.
*/
public interface WebAuthnService {
/**
* Use to start an authentication process with webauthn for the new user argument.
*
* @param loginId the end-user login id
* @param user the user details
* @param origin the origin of the URL for the web page where the webauthn operation is taking place, as returned
* by calling document.location.origin via javascript.
* @return transaction id response on success.
* @throws DescopeException on failure of any kind
*/
WebAuthnTransactionResponse signUpStart(String loginId, User user, String origin) throws DescopeException;

/**
* Use to finish an authentication process with a given transaction id and credentials after been signed
* by the credentials navigator.
*
* @param finishRequest the browser finish response
* @return {@link AuthenticationInfo} for a successful response
* @throws DescopeException on failure of any kind
*/
AuthenticationInfo signUpFinish(WebAuthnFinishRequest finishRequest) throws DescopeException;

/**
* Use to start an authentication validation with webauthn for an existing user with the given loginID.
*
* @param loginId the end-user login id
* @param origin the origin of the URL for the web page where the webauthn operation is taking place, as returned
* by calling document.location.origin via javascript.
* @param token when doing step-up or mfa then we need current session token
* @param loginOptions {@link LoginOptions LoginOptions}
* @return transaction id response on success
* @throws DescopeException on failure of any kind
*/
WebAuthnTransactionResponse signInStart(String loginId, String origin, String token, LoginOptions loginOptions)
throws DescopeException;

/**
* Use to finish an authentication process with a given transaction id and credentials after been signed
* by the credentials navigator.
*
* @param finishRequest the browser finish response
* @return {@link AuthenticationInfo} for a successful response
* @throws DescopeException on failure of any kind
*/
AuthenticationInfo signInFinish(WebAuthnFinishRequest finishRequest) throws DescopeException;

/**
* Use to start an authentication validation with webauthn.
* If user does not exist, a new user will be created with the given login ID.
* The create field in the response object determines which browser API should be called,
* either navigator.credentials.create or navigator.credentials.get as well as whether to call signUpFinish
* (if create is true) or signInFinish (if create is false) later to finalize the operation.
*
* @param loginId the end-user login id
* @param origin the origin of the URL for the web page where the webauthn operation is taking place, as returned
* by calling document.location.origin via javascript.
* @return transaction id response on success
* @throws DescopeException on failure of any kind
*/
WebAuthnTransactionResponse signUpOrInStart(String loginId, String origin) throws DescopeException;

/**
* Use to start an add webauthn device process for an existing user with the given loginId.
* Token is required to send it to Descope, for verification.
*
* @param loginId the end-user login id
* @param origin the origin of the URL for the web page where the webauthn operation is taking place, as returned
* by calling document.location.origin via javascript.
* @param token an existing refresh token must be established and the token sent to Descope
* @return transaction id response on success
* @throws DescopeException on failure of any kind
*/
WebAuthnTransactionResponse updateUserDeviceStart(String loginId, String origin, String token)
throws DescopeException;

/**
* Use to finish an add webauthn device process with a given transaction id and credentials after been signed
* by the credentials navigator.
*
* @param finishRequest the browser finish response
* @throws DescopeException on failure of any kind
*/
void updateUserDeviceFinish(WebAuthnFinishRequest finishRequest) throws DescopeException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public static AuthenticationServices buildServices(Client client, AuthParams aut
.passwordService(new PasswordServiceImpl(client, authParams))
.magicLinkService(new MagicLinkServiceImpl(client, authParams))
.enchantedLinkService(new EnchantedLinkServiceImpl(client, authParams))
.webAuthnService(new WebAuthnServiceImpl(client, authParams))
.build();
}
}
148 changes: 148 additions & 0 deletions src/main/java/com/descope/sdk/auth/impl/WebAuthnServiceImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package com.descope.sdk.auth.impl;

import static com.descope.literals.Routes.AuthEndPoints.WEBAUTHN_SIGN_IN_FINISH;
import static com.descope.literals.Routes.AuthEndPoints.WEBAUTHN_SIGN_IN_START;
import static com.descope.literals.Routes.AuthEndPoints.WEBAUTHN_SIGN_UP_FINISH;
import static com.descope.literals.Routes.AuthEndPoints.WEBAUTHN_SIGN_UP_OR_IN_START;
import static com.descope.literals.Routes.AuthEndPoints.WEBAUTHN_SIGN_UP_START;
import static com.descope.literals.Routes.AuthEndPoints.WEBAUTHN_UPDATE_FINISH;
import static com.descope.literals.Routes.AuthEndPoints.WEBAUTHN_UPDATE_START;
import static com.descope.utils.CollectionUtils.mapOf;

import com.descope.exception.DescopeException;
import com.descope.exception.ServerCommonException;
import com.descope.model.auth.AuthParams;
import com.descope.model.auth.AuthenticationInfo;
import com.descope.model.client.Client;
import com.descope.model.jwt.response.JWTResponse;
import com.descope.model.magiclink.LoginOptions;
import com.descope.model.user.User;
import com.descope.model.webauthn.WebAuthnFinishRequest;
import com.descope.model.webauthn.WebAuthnTransactionResponse;
import com.descope.proxy.ApiProxy;
import com.descope.sdk.auth.WebAuthnService;
import com.descope.utils.JwtUtils;
import java.net.URI;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;

public class WebAuthnServiceImpl extends AuthenticationServiceImpl implements WebAuthnService {

WebAuthnServiceImpl(Client client, AuthParams authParams) {
super(client, authParams);
}

@Override
public WebAuthnTransactionResponse signUpStart(String loginId, User user, String origin) throws DescopeException {
if (StringUtils.isBlank(loginId)) {
throw ServerCommonException.invalidArgument("Login ID");
}
if (StringUtils.isBlank(origin)) {
throw ServerCommonException.invalidArgument("Origin");
}
if (user == null) {
user = new User();
}
URI webAuthnSignUpURL = getUri(WEBAUTHN_SIGN_UP_START);
Map<String, Object> signUpRequest = mapOf("loginId", loginId, "user", user, "origin", origin);
ApiProxy apiProxy = getApiProxy();
return apiProxy.post(webAuthnSignUpURL, signUpRequest, WebAuthnTransactionResponse.class);
}

@Override
public AuthenticationInfo signUpFinish(WebAuthnFinishRequest finishRequest) throws DescopeException {
if (finishRequest == null
|| StringUtils.isBlank(finishRequest.getResponse())
|| StringUtils.isBlank(finishRequest.getTransactionId())) {
throw ServerCommonException.invalidArgument("Finish Request");
}
URI webAuthnSignUpURL = getUri(WEBAUTHN_SIGN_UP_FINISH);
ApiProxy apiProxy = getApiProxy();
JWTResponse jwtResponse = apiProxy.post(webAuthnSignUpURL, finishRequest, JWTResponse.class);
return getAuthenticationInfo(jwtResponse);
}

@Override
public WebAuthnTransactionResponse signInStart(String loginId, String origin, String token, LoginOptions loginOptions)
throws DescopeException {
if (StringUtils.isBlank(loginId)) {
throw ServerCommonException.invalidArgument("Login ID");
}
if (StringUtils.isBlank(origin)) {
throw ServerCommonException.invalidArgument("Origin");
}
ApiProxy apiProxy;
if (JwtUtils.isJWTRequired(loginOptions)) {
if (StringUtils.isBlank(token)) {
throw ServerCommonException.invalidArgument("Token");
}
apiProxy = getApiProxy(token);
} else {
apiProxy = getApiProxy();
}
URI webAuthnSignInURL = getUri(WEBAUTHN_SIGN_IN_START);
Map<String, Object> signInRequest = mapOf("loginId", loginId, "origin", origin);
if (loginOptions != null) {
signInRequest.put("loginOptions", loginOptions);
}
return apiProxy.post(webAuthnSignInURL, signInRequest, WebAuthnTransactionResponse.class);
}

@Override
public AuthenticationInfo signInFinish(WebAuthnFinishRequest finishRequest) throws DescopeException {
if (finishRequest == null
|| StringUtils.isBlank(finishRequest.getResponse())
|| StringUtils.isBlank(finishRequest.getTransactionId())) {
throw ServerCommonException.invalidArgument("Finish Request");
}
URI webAuthnSignInURL = getUri(WEBAUTHN_SIGN_IN_FINISH);
ApiProxy apiProxy = getApiProxy();
JWTResponse jwtResponse = apiProxy.post(webAuthnSignInURL, finishRequest, JWTResponse.class);
return getAuthenticationInfo(jwtResponse);
}

@Override
public WebAuthnTransactionResponse signUpOrInStart(String loginId, String origin) throws DescopeException {
if (StringUtils.isBlank(loginId)) {
throw ServerCommonException.invalidArgument("Login ID");
}
if (StringUtils.isBlank(origin)) {
throw ServerCommonException.invalidArgument("Origin");
}
URI webAuthnSignUpOrInURL = getUri(WEBAUTHN_SIGN_UP_OR_IN_START);
Map<String, Object> signUpOrInRequest = mapOf("loginId", loginId, "origin", origin);
ApiProxy apiProxy = getApiProxy();
return apiProxy.post(webAuthnSignUpOrInURL, signUpOrInRequest, WebAuthnTransactionResponse.class);
}

@Override
public WebAuthnTransactionResponse updateUserDeviceStart(String loginId, String origin, String token)
throws DescopeException {
if (StringUtils.isBlank(loginId)) {
throw ServerCommonException.invalidArgument("Login ID");
}
if (StringUtils.isBlank(origin)) {
throw ServerCommonException.invalidArgument("Origin");
}
if (StringUtils.isBlank(token)) {
throw ServerCommonException.invalidArgument("Token");
}
validateSessionWithToken(token); // no need to send remote if the token is not valid
URI webAuthnUpdateStartURL = getUri(WEBAUTHN_UPDATE_START);
Map<String, Object> updateRequest = mapOf("loginId", loginId, "origin", origin);
ApiProxy apiProxy = getApiProxy(token);
return apiProxy.post(webAuthnUpdateStartURL, updateRequest, WebAuthnTransactionResponse.class);
}

@Override
public void updateUserDeviceFinish(WebAuthnFinishRequest finishRequest) throws DescopeException {
if (finishRequest == null
|| StringUtils.isBlank(finishRequest.getResponse())
|| StringUtils.isBlank(finishRequest.getTransactionId())) {
throw ServerCommonException.invalidArgument("Finish Request");
}
URI webAuthnUpdateFinishURL = getUri(WEBAUTHN_UPDATE_FINISH);
ApiProxy apiProxy = getApiProxy();
apiProxy.post(webAuthnUpdateFinishURL, finishRequest, Void.class);
}
}
Loading