Skip to content

Commit

Permalink
feat: Update URL to allow user to complete WebAuthn registration in app
Browse files Browse the repository at this point in the history
The returnUrl/skipAction is determined by the presence of "self_registration" query param.
The "self_registration" parameter is introduced to distinguished in between self service and regular registration.
  • Loading branch information
tcompiegne authored and ashrafulmhasan committed Jan 30, 2022
1 parent f93b575 commit 35597fe
Show file tree
Hide file tree
Showing 8 changed files with 398 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ protected void doStart() throws Exception {
accountRouter.get(AccountRoutes.WEBAUTHN_CREDENTIALS_BY_ID.getRoute())
.handler(accountHandler::getUser)
.handler(accountWebAuthnCredentialsEndpointHandler::getEnrolledWebAuthnCredential);

accountRouter.delete(AccountRoutes.WEBAUTHN_CREDENTIALS_BY_ID.getRoute())
.handler(accountHandler::getUser)
.handler(accountWebAuthnCredentialsEndpointHandler::deleteWebAuthnCredential);
// error handler
accountRouter.route().failureHandler(new ErrorHandler());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,19 @@ public void getEnrolledWebAuthnCredential(RoutingContext routingContext) {
error -> routingContext.fail(error)
);
}

/**
* Delete enrolled WebAuthn credential detail for the current user
* @param routingContext the routingContext holding the current user
*/
public void deleteWebAuthnCredential(RoutingContext routingContext) {
final String id = routingContext.request().getParam("credentialId");

accountService.removeCredential(id)
.subscribe(
() -> AccountResponseHandler.handleNoBodyResponse(routingContext),
error -> routingContext.fail(error)
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,7 @@ public interface AccountService {
Single<List<Credential>> getWebAuthnCredentials(User user);

Single<Credential> getWebAuthnCredential(String id);

Completable removeCredential(String id);

}
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ public Single<Credential> getWebAuthnCredential(String id) {
});
}

@Override
public Completable removeCredential(String id) {
return credentialService.delete(id);
}

private io.gravitee.am.identityprovider.api.User convert(io.gravitee.am.model.User user) {
DefaultUser idpUser = new DefaultUser(user.getUsername());
idpUser.setId(user.getExternalId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,11 @@ private void renderPage(RoutingContext routingContext) {
final UserProperties userProperties = new UserProperties(user);

final String action = UriBuilderRequest.resolveProxyRequest(routingContext.request(), routingContext.request().path(), queryParams, true);
final String skipAction = UriBuilderRequest.resolveProxyRequest(routingContext.request(), routingContext.request().path(), queryParams.set("skipWebAuthN", "true"), true);


final String skipAction = queryParams.get("self_registration") != null
? queryParams.get("redirect_uri")
: UriBuilderRequest.resolveProxyRequest(routingContext.request(), routingContext.request().path(), queryParams.set("skipWebAuthN", "true"), true);

routingContext.put(ConstantKeys.ACTION_KEY, action);
routingContext.put(ConstantKeys.SKIP_ACTION_KEY, skipAction);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package io.gravitee.am.gateway.handler.root.resources.endpoint.webauthn;

import io.gravitee.am.common.jwt.Claims;
import io.gravitee.am.common.web.UriBuilder;
import io.gravitee.am.gateway.handler.common.auth.user.EndUserAuthentication;
import io.gravitee.am.gateway.handler.common.auth.user.UserAuthenticationManager;
import io.gravitee.am.common.utils.ConstantKeys;
Expand Down Expand Up @@ -150,9 +151,11 @@ public void handle(RoutingContext ctx) {
ctx.session().put(ConstantKeys.PASSWORDLESS_AUTH_COMPLETED_KEY, true);
ctx.session().put(ConstantKeys.WEBAUTHN_CREDENTIAL_ID_CONTEXT_KEY, credentialId);

// Now redirect back to authorization endpoint.
// Now redirect back the redirect_uri if self_registration param is present, otherwise redirect to authorization endpoint.
final MultiMap queryParams = RequestUtils.getCleanedQueryParams(ctx.request());
final String returnURL = UriBuilderRequest.resolveProxyRequest(ctx.request(), ctx.get(CONTEXT_PATH) + "/oauth/authorize", queryParams, true);
final String returnURL = queryParams.get("self_registration") != null
? queryParams.get("redirect_uri")
: UriBuilderRequest.resolveProxyRequest(ctx.request(), ctx.get(CONTEXT_PATH) + "/oauth/authorize", queryParams, true);
ctx.response().putHeader(HttpHeaders.LOCATION, returnURL).end();
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**
* Copyright (C) 2015 The Gravitee team (http://gravitee.io)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* 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.gravitee.am.gateway.handler.root.resources.endpoint.webauthn;

import io.gravitee.am.common.utils.ConstantKeys;
import io.gravitee.am.gateway.handler.common.auth.user.UserAuthenticationManager;
import io.gravitee.am.gateway.handler.common.client.ClientSyncService;
import io.gravitee.am.gateway.handler.common.vertx.RxWebTestBase;
import io.gravitee.am.gateway.handler.common.vertx.web.handler.ErrorHandler;
import io.gravitee.am.gateway.handler.root.resources.handler.client.ClientRequestParseHandler;
import io.gravitee.am.gateway.handler.root.resources.handler.webauthn.WebAuthnAccessHandler;
import io.gravitee.am.gateway.handler.vertx.auth.webauthn.GraviteeWebAuthnOptions;
import io.gravitee.am.model.Domain;
import io.gravitee.am.model.User;
import io.gravitee.am.model.login.LoginSettings;
import io.gravitee.am.model.oidc.Client;
import io.reactivex.Maybe;
import io.vertx.core.Handler;
import io.vertx.core.VertxOptions;
import io.vertx.core.http.HttpMethod;
import io.vertx.ext.auth.webauthn.RelyingParty;
import io.vertx.reactivex.core.Vertx;
import io.vertx.reactivex.ext.auth.webauthn.WebAuthn;
import io.vertx.reactivex.ext.web.RoutingContext;
import io.vertx.reactivex.ext.web.handler.BodyHandler;
import io.vertx.reactivex.ext.web.handler.SessionHandler;
import io.vertx.reactivex.ext.web.sstore.LocalSessionStore;
import io.vertx.reactivex.ext.web.templ.thymeleaf.ThymeleafTemplateEngine;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;

import java.util.Map;
import java.util.UUID;

import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class WebAuthnRegisterEndpointTest extends RxWebTestBase {

private static final String REQUEST_PATH = "/self-account-management-test/webauthn/register";

@Mock
private UserAuthenticationManager userAuthenticationManager;

@Mock
private Domain domain;

@Mock
private ThymeleafTemplateEngine templateEngine;

@Mock
private ClientSyncService clientSyncService;

private ClientRequestParseHandler clientRequestParseHandler;
private WebAuthnRegisterEndpoint webAuthnRegisterEndpoint;
private WebAuthnAccessHandler webAuthnAccessHandler;
private Client client;

@Override
public void setUp() throws Exception {
super.setUp();

clientRequestParseHandler = new ClientRequestParseHandler(clientSyncService).setRequired(true);
webAuthnAccessHandler = new WebAuthnAccessHandler(domain);
client = new Client();
client.setClientId(UUID.randomUUID().toString());
when(clientSyncService.findByClientId(client.getClientId())).thenReturn(Maybe.just(client));

final LoginSettings loginSettings = new LoginSettings();
loginSettings.setPasswordlessEnabled(true);
when(domain.getLoginSettings()).thenReturn(loginSettings);

final User user = new User();
final VertxOptions options = getOptions();
final Vertx vertx = Vertx.vertx(options);

final WebAuthn webAuthnImpl = WebAuthn.create(vertx, new GraviteeWebAuthnOptions().setRelyingParty(new RelyingParty().setName("Some Party Name")));
final SessionHandler sessionHandler = SessionHandler.create(LocalSessionStore.create(vertx));

webAuthnRegisterEndpoint = new WebAuthnRegisterEndpoint(domain, userAuthenticationManager, webAuthnImpl, this.templateEngine);

router.route()
.order(-1)
.handler(sessionHandler)
.handler(ctx -> {
ctx.setUser(io.vertx.reactivex.ext.auth.User.newInstance(new io.gravitee.am.gateway.handler.common.vertx.web.auth.user.User(user)));
ctx.next();
})
.handler(BodyHandler.create())
.failureHandler(new ErrorHandler());

}

@Test
public void shouldNotRedirectToRegisterEndpoint_for_selfRegistration() throws Exception {
router.route(REQUEST_PATH)
.handler(clientRequestParseHandler)
.handler(webAuthnAccessHandler)
.handler(renderPageWithRedirectUri())
.handler(rc -> rc.response().end());

testRequest(HttpMethod.GET,
REQUEST_PATH + "?client_id=" + client.getClientId() + "&redirect_uri=http://redirect.com/app&self_registration=true",
req -> req.headers().set("content-type", "application/json"),
200,
"OK", null);
}

@Test
public void shouldRedirectToRegisterEndpoint() throws Exception {
router.route(REQUEST_PATH)
.handler(clientRequestParseHandler)
.handler(webAuthnAccessHandler)
.handler(renderPageWithRegisterEndpointUri())
.handler(rc -> rc.response().end());

testRequest(HttpMethod.GET,
REQUEST_PATH + "?client_id=" + client.getClientId() + "&redirect_uri=http://redirect.com/app",
req -> req.headers().set("content-type", "application/json"),
200,
"OK", null);
}

private Handler<RoutingContext> renderPageWithRedirectUri() {
return routingContext -> {
doAnswer(answer -> {
final String skipAction = routingContext.get(ConstantKeys.SKIP_ACTION_KEY);
assertEquals("skip action should be the redirect uri", "http://redirect.com/app", skipAction);
assertFalse("skip action should not contain", skipAction.contains("/webauthn/register"));

routingContext.next();
return answer;
}).when(templateEngine).render(Mockito.<Map<String, Object>>any(), Mockito.any(), Mockito.any());

webAuthnRegisterEndpoint.handle(routingContext);
};
}

private Handler<RoutingContext> renderPageWithRegisterEndpointUri() {
return routingContext -> {
doAnswer(answer -> {
final String skipAction = routingContext.get(ConstantKeys.SKIP_ACTION_KEY);
assertTrue("skip action should contain", skipAction.contains("/webauthn/register"));
assertTrue("skip action contain the redirect uri", skipAction.contains("redirect_uri=http%3A%2F%2Fredirect.com%2Fapp"));

routingContext.next();
return answer;
}).when(templateEngine).render(Mockito.<Map<String, Object>>any(), Mockito.any(), Mockito.any());

webAuthnRegisterEndpoint.handle(routingContext);
};
}
}
Loading

0 comments on commit 35597fe

Please sign in to comment.