Skip to content
This repository has been archived by the owner on Oct 15, 2020. It is now read-only.

Commit

Permalink
support Security Key
Browse files Browse the repository at this point in the history
  • Loading branch information
tnorimat committed Apr 11, 2019
1 parent 5a5a13b commit 6ec6855
Show file tree
Hide file tree
Showing 22 changed files with 565 additions and 36 deletions.
50 changes: 49 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,52 @@
# Intellij
###################
.idea
*.iml

target/
# Eclipse #
###########
.project
.settings
.classpath
.factorypath


# NetBeans #
############
nbactions.xml
nb-configuration.xml
catalog.xml
nbproject

# Compiled source #
###################
*.com
*.class
*.dll
*.exe
*.o
*.so

# Packages #
############
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip

# Logs and databases #
######################
*.log

# Maven #
#########
target
target/

# Maven shade
#############
*dependency-reduced-pom.xml
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,35 @@

[Web Authentication](https://www.w3.org/TR/webauthn/)(WebAuthn) sample plugin for [Keycloak](https://www.keycloak.org) , implements with [webauthn4j](https://github.com/webauthn4j/webauthn4j).

## Environment

We've confirmed that this demo had worked well under the following environments:

- 2 Factor Authentication with Resident Key Not supported Authenticator Scenario

- OS : Windows 10
- Browser : Google Chrome (ver 73), Mozilla FireFox (ver 66)
- Authenticator : Yubico Security Key
- Server(RP) : keycloak-5.0.0 on localhost

- 2 Factor Authentication with Resident Key supported Authenticator Scenario

- OS : Windows 10
- Browser : Microsoft Edge (ver 44)
- Authenticator : Internal Fingerprint Authentication Device
- Server(RP) : keycloak-5.0.0 on localhost

- Authentication with Resident Key supported Authenticator Scenario

- OS : Windows 10
- Browser : Microsoft Edge (ver 44)
- Authenticator : Internal Fingerprint Authentication Device
- Server(RP) : keycloak-5.0.0 on localhost

## Install

Please checkout the branch 'demo-completed'.

- build

- `$ mvn install`
Expand Down Expand Up @@ -51,6 +78,14 @@
| Identity Provider Redirector | | ALTERNATIVE |
| WebAutn Authenticator | | REQUIRED |

## Open Issue

We've not yet resolved the following issues:

- credential storage : avoid creating a new table for credentials, mentioned in [keycloak-dev ML](http://lists.jboss.org/pipermail/keycloak-dev/2019-March/011837.html).
- problem on re-build and re-deploy : after re-building and re-deploying this demo, there are some cases that the user registered before this re-building and re-deploying can not be authenticated.
- latest WebAuthn4j support : this demo has not yet supported the latest WebAuthn4j(0.9.2.RELEASE)

## TODO

- [x] Unit Test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* 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 org.keycloak.authenticator;

import com.webauthn4j.response.WebAuthnRegistrationContext;
Expand All @@ -7,7 +24,11 @@
import com.webauthn4j.server.ServerProperty;
import com.webauthn4j.validator.WebAuthnRegistrationContextValidationResponse;
import com.webauthn4j.validator.WebAuthnRegistrationContextValidator;

import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.AuthenticationFlowException;
import org.keycloak.authentication.Authenticator;
import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.UriUtils;
Expand All @@ -23,7 +44,7 @@
import java.util.Map;

public class RegisterAuthenticator implements Authenticator {

private static final Logger logger = Logger.getLogger(RegisterAuthenticator.class);

private static final String AUTH_NOTE = "WEBAUTH_CHALLENGE";

Expand Down Expand Up @@ -66,10 +87,20 @@ public void authenticate(AuthenticationFlowContext context) {
public void action(AuthenticationFlowContext context) {

MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters();

// receive error from navigator.credentials.create()
String error = params.getFirst("error");
if (error != null && !error.isEmpty()) {
throw new AuthenticationFlowException("exception raised from navigator.credentials.create() : " + error, AuthenticationFlowError.INVALID_CREDENTIALS);
}

String baseUrl = UriUtils.getOrigin(context.getUriInfo().getBaseUri());
String rpId = context.getUriInfo().getBaseUri().getHost();
byte[] clientDataJSON = Base64.getUrlDecoder().decode(params.getFirst("clientDataJSON"));
byte[] attestationObject = Base64.getUrlDecoder().decode(params.getFirst("attestationObject"));
String publicKeyCredentialId = params.getFirst("publicKeyCredentialId");
context.getUser().setSingleAttribute("PUBLIC_KEY_CREDENTIAL_ID", publicKeyCredentialId);
logger.debugv("publicKeyCredentialId = {0}", context.getUser().getAttribute("PUBLIC_KEY_CREDENTIAL_ID").get(0));

Origin origin = new Origin(baseUrl);
Challenge challenge = new DefaultChallenge(context.getAuthenticationSession().getAuthNote(AUTH_NOTE));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* 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 org.keycloak.authenticator;

import org.keycloak.Config;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
package org.keycloak.authenticator;
/*
* Copyright 2002-2019 the original author or authors.
*
* 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 org.keycloak.authenticator;

import com.webauthn4j.response.WebAuthnAuthenticationContext;
import com.webauthn4j.response.client.Origin;
import com.webauthn4j.response.client.challenge.Challenge;
import com.webauthn4j.response.client.challenge.DefaultChallenge;
import com.webauthn4j.server.ServerProperty;

import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.AuthenticationFlowException;
import org.keycloak.authentication.Authenticator;
import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.UriUtils;
Expand All @@ -23,6 +42,8 @@
import java.util.Map;

public class WebAuthn4jAuthenticator implements Authenticator {
private static final Logger logger = Logger.getLogger(WebAuthn4jAuthenticator.class);

private static final String AUTH_NOTE = "WEBAUTH_CHALLENGE";

private KeycloakSession session;
Expand All @@ -41,11 +62,19 @@ private Map<String, String> generateParameters(RealmModel realm, URI baseUri) {
return params;
}


public void authenticate(AuthenticationFlowContext context) {
LoginFormsProvider form = context.form();
Map<String, String> params = generateParameters(context.getRealm(), context.getUriInfo().getBaseUri());
context.getAuthenticationSession().setAuthNote(AUTH_NOTE, params.get("challenge"));
if (context.getUser() != null) {
// in U2F Scenario
String publicKeyCredentialId = context.getUser().getAttribute("PUBLIC_KEY_CREDENTIAL_ID").get(0);
logger.debugv("publicKeyCredentialId = {0}", publicKeyCredentialId);
params.put("publicKeyCredentialId", publicKeyCredentialId);
} else {
// in UAF Scenario
params.put("publicKeyCredentialId", "");
}
params.forEach(form::setAttribute);
context.challenge(form.createForm("webauthn.ftl"));
}
Expand All @@ -54,6 +83,12 @@ public void action(AuthenticationFlowContext context) {

MultivaluedMap<String, String> params = context.getHttpRequest().getDecodedFormParameters();

// receive error from navigator.credentials.get()
String error = params.getFirst("error");
if (error != null && !error.isEmpty()) {
throw new AuthenticationFlowException("exception raised from navigator.credentials.get() : " + error, AuthenticationFlowError.INVALID_USER);
}

String baseUrl = UriUtils.getOrigin(context.getUriInfo().getBaseUri());
String rpId = context.getUriInfo().getBaseUri().getHost();

Expand All @@ -67,20 +102,45 @@ public void action(AuthenticationFlowContext context) {
byte[] signature = Base64Url.decode(params.getFirst("signature"));

String userId = params.getFirst("userHandle");
boolean isUVFlagChecked = true;
logger.debugv("userId = {0}", userId);

if (userId == null || userId.isEmpty()) {
// in U2F win Resident Key not supported Authenticator Scenario
userId = context.getUser().getId();
isUVFlagChecked = false;
} else {
if (context.getUser() != null) {
// in U2F with Resident Key supported Authenticator Scenario
String firstAuthenticatedUserId = context.getUser().getId();
logger.debugv("firstAuthenticatedUserId = {0}", firstAuthenticatedUserId);
if (firstAuthenticatedUserId != null && !firstAuthenticatedUserId.equals(userId)) {
throw new AuthenticationFlowException("First authenticated user is not the one authenticated by 2nd factor authenticator", AuthenticationFlowError.USER_CONFLICT);
}
} else {
// NOP
// in UAF with Resident Key supported Authenticator Scenario
}
}
UserModel user = session.users().getUserById(userId, context.getRealm());
WebAuthnAuthenticationContext authenticationContext = new WebAuthnAuthenticationContext(
credentialId,
clientDataJSON,
authenticatorData,
signature,
server,
true
isUVFlagChecked
);

WebAuthnCredentialModel cred = new WebAuthnCredentialModel();
cred.setAuthenticationContext(authenticationContext);

boolean result = session.userCredentialManager().isValid(context.getRealm(), user, cred);
boolean result = false;
try {
result = session.userCredentialManager().isValid(context.getRealm(), user, cred);
} catch (Exception e) {
throw new AuthenticationFlowException("unknown user authenticated by the authenticator", AuthenticationFlowError.UNKNOWN_USER);
}
if (result) {
context.setUser(user);
context.success();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* 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 org.keycloak.authenticator;

import org.keycloak.Config;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
package org.keycloak.credential;
/*
* Copyright 2002-2019 the original author or authors.
*
* 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 org.keycloak.credential;

import com.webauthn4j.response.WebAuthnAuthenticationContext;
import com.webauthn4j.response.attestation.authenticator.AttestedCredentialData;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* 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 org.keycloak.credential;

import com.webauthn4j.authenticator.Authenticator;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* 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 org.keycloak.credential;

import org.keycloak.models.KeycloakSession;
Expand Down
Loading

0 comments on commit 6ec6855

Please sign in to comment.