diff --git a/src/main/java/com/descope/literals/Routes.java b/src/main/java/com/descope/literals/Routes.java index fdde0bfb..c99667be 100644 --- a/src/main/java/com/descope/literals/Routes.java +++ b/src/main/java/com/descope/literals/Routes.java @@ -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"; diff --git a/src/main/java/com/descope/model/auth/AuthenticationServices.java b/src/main/java/com/descope/model/auth/AuthenticationServices.java index 7ba0f5f4..e6a58cc2 100644 --- a/src/main/java/com/descope/model/auth/AuthenticationServices.java +++ b/src/main/java/com/descope/model/auth/AuthenticationServices.java @@ -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; @@ -22,4 +23,5 @@ public class AuthenticationServices { PasswordService passwordService; MagicLinkService magicLinkService; EnchantedLinkService enchantedLinkService; + WebAuthnService webAuthnService; } diff --git a/src/main/java/com/descope/model/webauthn/WebAuthnFinishRequest.java b/src/main/java/com/descope/model/webauthn/WebAuthnFinishRequest.java new file mode 100644 index 00000000..25242f8f --- /dev/null +++ b/src/main/java/com/descope/model/webauthn/WebAuthnFinishRequest.java @@ -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; +} diff --git a/src/main/java/com/descope/model/webauthn/WebAuthnTransactionResponse.java b/src/main/java/com/descope/model/webauthn/WebAuthnTransactionResponse.java new file mode 100644 index 00000000..3f7d0c98 --- /dev/null +++ b/src/main/java/com/descope/model/webauthn/WebAuthnTransactionResponse.java @@ -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; +} diff --git a/src/main/java/com/descope/sdk/auth/WebAuthnService.java b/src/main/java/com/descope/sdk/auth/WebAuthnService.java new file mode 100644 index 00000000..3e7e159d --- /dev/null +++ b/src/main/java/com/descope/sdk/auth/WebAuthnService.java @@ -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; +} diff --git a/src/main/java/com/descope/sdk/auth/impl/AuthenticationServiceBuilder.java b/src/main/java/com/descope/sdk/auth/impl/AuthenticationServiceBuilder.java index a1fb317e..5063619d 100644 --- a/src/main/java/com/descope/sdk/auth/impl/AuthenticationServiceBuilder.java +++ b/src/main/java/com/descope/sdk/auth/impl/AuthenticationServiceBuilder.java @@ -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(); } } diff --git a/src/main/java/com/descope/sdk/auth/impl/WebAuthnServiceImpl.java b/src/main/java/com/descope/sdk/auth/impl/WebAuthnServiceImpl.java new file mode 100644 index 00000000..0dc31613 --- /dev/null +++ b/src/main/java/com/descope/sdk/auth/impl/WebAuthnServiceImpl.java @@ -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 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 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 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 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); + } +} diff --git a/src/test/java/com/descope/sdk/auth/impl/WebAuthnServiceImplTest.java b/src/test/java/com/descope/sdk/auth/impl/WebAuthnServiceImplTest.java new file mode 100644 index 00000000..e07d891b --- /dev/null +++ b/src/test/java/com/descope/sdk/auth/impl/WebAuthnServiceImplTest.java @@ -0,0 +1,349 @@ +package com.descope.sdk.auth.impl; + +import static com.descope.sdk.TestUtils.MOCK_EMAIL; +import static com.descope.sdk.TestUtils.MOCK_JWT_RESPONSE; +import static com.descope.sdk.TestUtils.MOCK_NAME; +import static com.descope.sdk.TestUtils.MOCK_PHONE; +import static com.descope.sdk.TestUtils.MOCK_SIGNING_KEY; +import static com.descope.sdk.TestUtils.MOCK_TOKEN; +import static com.descope.sdk.TestUtils.PROJECT_ID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +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.Provider; +import com.descope.model.jwt.Token; +import com.descope.model.jwt.response.SigningKeysResponse; +import com.descope.model.magiclink.LoginOptions; +import com.descope.model.user.User; +import com.descope.model.user.response.UserResponse; +import com.descope.model.webauthn.WebAuthnFinishRequest; +import com.descope.model.webauthn.WebAuthnTransactionResponse; +import com.descope.proxy.ApiProxy; +import com.descope.proxy.impl.ApiProxyBuilder; +import com.descope.sdk.TestUtils; +import com.descope.sdk.auth.WebAuthnService; +import com.descope.utils.JwtUtils; +import java.security.Key; +import java.util.Arrays; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +public class WebAuthnServiceImplTest { + + private WebAuthnService webAuthnService; + + @BeforeEach + void setUp() { + AuthParams authParams = TestUtils.getAuthParams(); + Client client = TestUtils.getClient(); + this.webAuthnService = + AuthenticationServiceBuilder.buildServices(client, authParams).getWebAuthnService(); + } + + @Test + void testSignUpStartForSuccess() { + User user = new User(MOCK_NAME, MOCK_EMAIL, MOCK_PHONE); + ApiProxy apiProxy = mock(ApiProxy.class); + WebAuthnTransactionResponse response = new WebAuthnTransactionResponse("t1", "o", false); + doReturn(response).when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + WebAuthnTransactionResponse signUp = webAuthnService.signUpStart(MOCK_EMAIL, user, "kuku"); + assertThat(signUp).isNotNull(); + assertEquals("t1", signUp.getTransactionId()); + } + } + + @Test + void testSignUpStartEmptyLoginId() { + ServerCommonException thrown = + assertThrows(ServerCommonException.class, + () -> webAuthnService.signUpStart(null, null, null)); + assertNotNull(thrown); + assertEquals("The Login ID argument is invalid", thrown.getMessage()); + } + + @Test + void testSignUpStartEmptyOrigin() { + ServerCommonException thrown = + assertThrows(ServerCommonException.class, + () -> webAuthnService.signUpStart("x", null, null)); + assertNotNull(thrown); + assertEquals("The Origin argument is invalid", thrown.getMessage()); + } + + @Test + void testSignUpFinishForSuccess() { + ApiProxy apiProxy = mock(ApiProxy.class); + doReturn(MOCK_JWT_RESPONSE).when(apiProxy).post(any(), any(), any()); + doReturn(new SigningKeysResponse(Arrays.asList(MOCK_SIGNING_KEY))) + .when(apiProxy).get(any(), eq(SigningKeysResponse.class)); + Provider provider = mock(Provider.class); + when(provider.getProvidedKey()).thenReturn(mock(Key.class)); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + try (MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + mockedJwtUtils.when(() -> JwtUtils.getToken(anyString(), any())).thenReturn(MOCK_TOKEN); + AuthenticationInfo authenticationInfo = webAuthnService.signUpFinish(new WebAuthnFinishRequest("t1", "r")); + assertThat(authenticationInfo).isNotNull(); + Token sessionToken = authenticationInfo.getToken(); + assertThat(sessionToken).isNotNull(); + assertThat(sessionToken.getJwt()).isNotBlank(); + assertThat(sessionToken.getClaims()).isNotEmpty(); + assertThat(sessionToken.getProjectId()).isEqualTo(PROJECT_ID); + Token refreshToken = authenticationInfo.getRefreshToken(); + assertThat(refreshToken).isNotNull(); + assertThat(refreshToken.getJwt()).isNotBlank(); + assertThat(refreshToken.getClaims()).isNotEmpty(); + assertThat(refreshToken.getProjectId()).isEqualTo(PROJECT_ID); + UserResponse user = authenticationInfo.getUser(); + assertThat(user).isNotNull(); + assertThat(user.getUserId()).isNotBlank(); + assertThat(user.getLoginIds()).isNotEmpty(); + } + } + } + + @Test + void testSignUpFinishEmptyRequest() { + ServerCommonException thrown = + assertThrows(ServerCommonException.class, + () -> webAuthnService.signUpFinish(null)); + assertNotNull(thrown); + assertEquals("The Finish Request argument is invalid", thrown.getMessage()); + thrown = + assertThrows(ServerCommonException.class, + () -> webAuthnService.signUpFinish(new WebAuthnFinishRequest(null, null))); + assertNotNull(thrown); + assertEquals("The Finish Request argument is invalid", thrown.getMessage()); + thrown = + assertThrows(ServerCommonException.class, + () -> webAuthnService.signUpFinish(new WebAuthnFinishRequest("kuku", null))); + assertNotNull(thrown); + assertEquals("The Finish Request argument is invalid", thrown.getMessage()); + } + + @Test + void testSignInStartForSuccess() { + ApiProxy apiProxy = mock(ApiProxy.class); + WebAuthnTransactionResponse response = new WebAuthnTransactionResponse("t1", "o", false); + doReturn(response).when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + WebAuthnTransactionResponse signUp = webAuthnService.signInStart(MOCK_EMAIL, "kuku", null, null); + assertThat(signUp).isNotNull(); + assertEquals("t1", signUp.getTransactionId()); + } + } + + @Test + void testSignInStartEmptyLoginId() { + ServerCommonException thrown = + assertThrows(ServerCommonException.class, + () -> webAuthnService.signInStart(null, null, null, null)); + assertNotNull(thrown); + assertEquals("The Login ID argument is invalid", thrown.getMessage()); + } + + @Test + void testSignInStartEmptyOrigin() { + ServerCommonException thrown = + assertThrows(ServerCommonException.class, + () -> webAuthnService.signInStart("x", null, null, null)); + assertNotNull(thrown); + assertEquals("The Origin argument is invalid", thrown.getMessage()); + } + + @Test + void testSignInStartEmptyToken() { + ServerCommonException thrown = + assertThrows(ServerCommonException.class, + () -> webAuthnService.signInStart("x", "x", null, new LoginOptions(true, false, null))); + assertNotNull(thrown); + assertEquals("The Token argument is invalid", thrown.getMessage()); + } + + @Test + void testSignInFinishForSuccess() { + ApiProxy apiProxy = mock(ApiProxy.class); + doReturn(MOCK_JWT_RESPONSE).when(apiProxy).post(any(), any(), any()); + doReturn(new SigningKeysResponse(Arrays.asList(MOCK_SIGNING_KEY))) + .when(apiProxy).get(any(), eq(SigningKeysResponse.class)); + Provider provider = mock(Provider.class); + when(provider.getProvidedKey()).thenReturn(mock(Key.class)); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + try (MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + mockedJwtUtils.when(() -> JwtUtils.getToken(anyString(), any())).thenReturn(MOCK_TOKEN); + AuthenticationInfo authenticationInfo = webAuthnService.signInFinish(new WebAuthnFinishRequest("t1", "r")); + assertThat(authenticationInfo).isNotNull(); + Token sessionToken = authenticationInfo.getToken(); + assertThat(sessionToken).isNotNull(); + assertThat(sessionToken.getJwt()).isNotBlank(); + assertThat(sessionToken.getClaims()).isNotEmpty(); + assertThat(sessionToken.getProjectId()).isEqualTo(PROJECT_ID); + Token refreshToken = authenticationInfo.getRefreshToken(); + assertThat(refreshToken).isNotNull(); + assertThat(refreshToken.getJwt()).isNotBlank(); + assertThat(refreshToken.getClaims()).isNotEmpty(); + assertThat(refreshToken.getProjectId()).isEqualTo(PROJECT_ID); + UserResponse user = authenticationInfo.getUser(); + assertThat(user).isNotNull(); + assertThat(user.getUserId()).isNotBlank(); + assertThat(user.getLoginIds()).isNotEmpty(); + } + } + } + + @Test + void testSignInFinishEmptyRequest() { + ServerCommonException thrown = + assertThrows(ServerCommonException.class, + () -> webAuthnService.signInFinish(null)); + assertNotNull(thrown); + assertEquals("The Finish Request argument is invalid", thrown.getMessage()); + thrown = + assertThrows(ServerCommonException.class, + () -> webAuthnService.signInFinish(new WebAuthnFinishRequest(null, null))); + assertNotNull(thrown); + assertEquals("The Finish Request argument is invalid", thrown.getMessage()); + thrown = + assertThrows(ServerCommonException.class, + () -> webAuthnService.signInFinish(new WebAuthnFinishRequest("kuku", null))); + assertNotNull(thrown); + assertEquals("The Finish Request argument is invalid", thrown.getMessage()); + } + + @Test + void testSignUpOrInStartForSuccess() { + ApiProxy apiProxy = mock(ApiProxy.class); + WebAuthnTransactionResponse response = new WebAuthnTransactionResponse("t1", "o", false); + doReturn(response).when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + WebAuthnTransactionResponse signUp = webAuthnService.signUpOrInStart(MOCK_EMAIL, "kuku"); + assertThat(signUp).isNotNull(); + assertEquals("t1", signUp.getTransactionId()); + } + } + + @Test + void testSignUpOrInStartEmptyLoginId() { + ServerCommonException thrown = + assertThrows(ServerCommonException.class, + () -> webAuthnService.signUpOrInStart(null, null)); + assertNotNull(thrown); + assertEquals("The Login ID argument is invalid", thrown.getMessage()); + } + + @Test + void testSignUpOrInStartEmptyOrigin() { + ServerCommonException thrown = + assertThrows(ServerCommonException.class, + () -> webAuthnService.signUpOrInStart("x", null)); + assertNotNull(thrown); + assertEquals("The Origin argument is invalid", thrown.getMessage()); + } + + @Test + void testUpdateUserDeviceStartForSuccess() { + ApiProxy apiProxy = mock(ApiProxy.class); + WebAuthnTransactionResponse response = new WebAuthnTransactionResponse("t1", "o", false); + doReturn(response).when(apiProxy).post(any(), any(), any()); + doReturn(new SigningKeysResponse(Arrays.asList(MOCK_SIGNING_KEY))) + .when(apiProxy).get(any(), eq(SigningKeysResponse.class)); + Provider provider = mock(Provider.class); + when(provider.getProvidedKey()).thenReturn(mock(Key.class)); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + try (MockedStatic mockedJwtUtils = mockStatic(JwtUtils.class)) { + mockedJwtUtils.when(() -> JwtUtils.getToken(anyString(), any())).thenReturn(MOCK_TOKEN); + WebAuthnTransactionResponse signUp = webAuthnService.updateUserDeviceStart(MOCK_EMAIL, "kuku", "kiki"); + assertThat(signUp).isNotNull(); + assertEquals("t1", signUp.getTransactionId()); + } + } + } + + @Test + void testUpdateUserDeviceStartEmptyLoginId() { + ServerCommonException thrown = + assertThrows(ServerCommonException.class, + () -> webAuthnService.updateUserDeviceStart(null, null, null)); + assertNotNull(thrown); + assertEquals("The Login ID argument is invalid", thrown.getMessage()); + } + + @Test + void testUpdateUserDeviceStartEmptyOrigin() { + ServerCommonException thrown = + assertThrows(ServerCommonException.class, + () -> webAuthnService.updateUserDeviceStart("x", null, null)); + assertNotNull(thrown); + assertEquals("The Origin argument is invalid", thrown.getMessage()); + } + + @Test + void testUpdateUserDeviceStartEmptyToken() { + ServerCommonException thrown = + assertThrows(ServerCommonException.class, + () -> webAuthnService.updateUserDeviceStart("x", "x", null)); + assertNotNull(thrown); + assertEquals("The Token argument is invalid", thrown.getMessage()); + } + + @Test + void testUpdateUserDeviceFinishForSuccess() { + ApiProxy apiProxy = mock(ApiProxy.class); + doReturn(MOCK_JWT_RESPONSE).when(apiProxy).post(any(), any(), any()); + doReturn(new SigningKeysResponse(Arrays.asList(MOCK_SIGNING_KEY))) + .when(apiProxy).get(any(), eq(SigningKeysResponse.class)); + Provider provider = mock(Provider.class); + when(provider.getProvidedKey()).thenReturn(mock(Key.class)); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + webAuthnService.updateUserDeviceFinish(new WebAuthnFinishRequest("t1", "r")); + } + } + + @Test + void testUpdateUserDeviceFinishEmptyRequest() { + ServerCommonException thrown = + assertThrows(ServerCommonException.class, + () -> webAuthnService.updateUserDeviceFinish(null)); + assertNotNull(thrown); + assertEquals("The Finish Request argument is invalid", thrown.getMessage()); + thrown = + assertThrows(ServerCommonException.class, + () -> webAuthnService.updateUserDeviceFinish(new WebAuthnFinishRequest(null, null))); + assertNotNull(thrown); + assertEquals("The Finish Request argument is invalid", thrown.getMessage()); + thrown = + assertThrows(ServerCommonException.class, + () -> webAuthnService.updateUserDeviceFinish(new WebAuthnFinishRequest("kuku", null))); + assertNotNull(thrown); + assertEquals("The Finish Request argument is invalid", thrown.getMessage()); + } + +}