KEYCLOAK-7675 SPI and default implementation for Device User Code.

Author:    Hiroyuki Wada <h2-wada@nri.co.jp>
Date:      Sun May 12 15:47:15 2019 +0900

Signed-off-by: Łukasz Dywicki <luke@code-house.org>
This commit is contained in:
Hiroyuki Wada 2019-05-12 15:47:15 +09:00 committed by Pedro Igor
parent 9d57b88dba
commit 5edf14944e
8 changed files with 263 additions and 9 deletions

View file

@ -0,0 +1,56 @@
/*
* Copyright 2019 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.models;
import org.keycloak.common.util.RandomString;
import java.security.SecureRandom;
/**
* The default implementation for generating/formatting user code of OAuth 2.0 Device Authorization Grant.
* For generation, uppercase eight-letter format is used.
* For display, uppercase four-letters dashes four-letters format is used.
*
* @author <a href="mailto:h2-wada@nri.co.jp">Hiroyuki Wada</a>
*/
public class DefaultOAuth2DeviceUserCodeProvider implements OAuth2DeviceUserCodeProvider {
private static final int LENGTH = 8;
private static final String DELIMITER = "-";
@Override
public String generate() {
// For case-insensitive, use uppercase
return new RandomString(LENGTH, new SecureRandom(), RandomString.upper).nextString();
}
@Override
public String display(String userCode) {
return new StringBuilder(userCode).insert(4, DELIMITER).toString();
}
@Override
public String format(String userCode) {
return String.join("", userCode.split(DELIMITER)).toUpperCase();
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,51 @@
/*
* Copyright 2019 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.models;
import org.keycloak.Config;
/**
* @author <a href="mailto:h2-wada@nri.co.jp">Hiroyuki Wada</a>
*/
public class DefaultOAuth2DeviceUserCodeProviderFactory implements OAuth2DeviceUserCodeProviderFactory {
@Override
public OAuth2DeviceUserCodeProvider create(KeycloakSession session) {
return new DefaultOAuth2DeviceUserCodeProvider();
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "default";
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright 2019 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.models;
import org.keycloak.provider.Provider;
/**
* @author <a href="mailto:h2-wada@nri.co.jp">Hiroyuki Wada</a>
*/
public interface OAuth2DeviceUserCodeProvider extends Provider {
/**
* Generate a new user code for OAuth 2.0 Device Authorization Grant.
*
* @return Return a generated user code
*/
String generate();
/**
* Get human-readability user code from original user code.
*
* @param userCode Original user code
* @return Return a human-readability user code
*/
String display(String userCode);
/**
* Format inputted user code.
*
* @param userCode Inputted user code.
* @return
*/
String format(String userCode);
}

View file

@ -0,0 +1,26 @@
/*
* Copyright 2019 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.models;
import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:h2-wada@nri.co.jp">Hiroyuki Wada</a>
*/
public interface OAuth2DeviceUserCodeProviderFactory extends ProviderFactory<OAuth2DeviceUserCodeProvider> {
}

View file

@ -0,0 +1,50 @@
/*
* Copyright 2019 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.models;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:h2-wada@nri.co.jp">Hiroyuki Wada</a>
*/
public class OAuth2DeviceUserCodeSpi implements Spi {
public static final String NAME = "oauth2DeviceUserCode";
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return NAME;
}
@Override
public Class<? extends Provider> getProviderClass(){
return OAuth2DeviceUserCodeProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return OAuth2DeviceUserCodeProviderFactory.class;
}
}

View file

@ -0,0 +1,18 @@
#
# Copyright 2019 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# 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.
#
org.keycloak.models.DefaultOAuth2DeviceUserCodeProviderFactory

View file

@ -26,6 +26,7 @@ org.keycloak.models.RoleSpi
org.keycloak.models.ActionTokenStoreSpi
org.keycloak.models.CodeToTokenStoreSpi
org.keycloak.models.OAuth2DeviceTokenStoreSpi
org.keycloak.models.OAuth2DeviceUserCodeSpi
org.keycloak.models.SingleUseTokenStoreSpi
org.keycloak.models.TokenRevocationStoreSpi
org.keycloak.models.UserSessionSpi

View file

@ -22,7 +22,6 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.RandomString;
import org.keycloak.common.util.Time;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.events.Details;
@ -35,6 +34,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.OAuth2DeviceCodeModel;
import org.keycloak.models.OAuth2DeviceTokenStoreProvider;
import org.keycloak.models.OAuth2DeviceUserCodeModel;
import org.keycloak.models.OAuth2DeviceUserCodeProvider;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.SystemClientUtil;
@ -50,7 +50,6 @@ import org.keycloak.services.ErrorPageException;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.Cors;
@ -58,7 +57,6 @@ import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel;
import org.keycloak.util.JsonSerialization;
import org.keycloak.util.TokenUtil;
@ -67,7 +65,6 @@ import javax.ws.rs.POST;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.security.SecureRandom;
import static org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint.LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX;
@ -138,8 +135,8 @@ public class OAuth2DeviceAuthorizationEndpoint extends AuthorizationEndpointBase
OAuth2DeviceCodeModel deviceCode = OAuth2DeviceCodeModel.create(realm, client,
Base64Url.encode(KeycloakModelUtils.generateSecret()), request.getScope(), request.getNonce());
// TODO Configure user code format
String secret = new RandomString(10, new SecureRandom(), RandomString.upper).nextString();
OAuth2DeviceUserCodeProvider userCodeProvider = session.getProvider(OAuth2DeviceUserCodeProvider.class);
String secret = userCodeProvider.generate();
OAuth2DeviceUserCodeModel userCode = new OAuth2DeviceUserCodeModel(realm,
deviceCode.getDeviceCode(),
secret);
@ -155,7 +152,7 @@ public class OAuth2DeviceAuthorizationEndpoint extends AuthorizationEndpointBase
OAuth2DeviceAuthorizationResponse response = new OAuth2DeviceAuthorizationResponse();
response.setDeviceCode(deviceCode.getDeviceCode());
response.setUserCode(secret);
response.setUserCode(userCodeProvider.display(secret));
response.setExpiresIn(expiresIn);
response.setInterval(interval);
response.setVerificationUri(deviceUrl);
@ -272,8 +269,13 @@ public class OAuth2DeviceAuthorizationEndpoint extends AuthorizationEndpointBase
return createVerificationPage(Messages.OAUTH2_DEVICE_INVALID_USER_CODE);
}
// Format inputted user code
OAuth2DeviceUserCodeProvider userCodeProvider = session.getProvider(OAuth2DeviceUserCodeProvider.class);
String formattedUserCode = userCodeProvider.format(userCode);
// Find the token from store
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
OAuth2DeviceCodeModel deviceCodeModel = store.getByUserCode(realm, userCode);
OAuth2DeviceCodeModel deviceCodeModel = store.getByUserCode(realm, formattedUserCode);
if (deviceCodeModel == null) {
event.error(Errors.INVALID_OAUTH2_USER_CODE);
return createVerificationPage(Messages.OAUTH2_DEVICE_INVALID_USER_CODE);
@ -289,7 +291,7 @@ public class OAuth2DeviceAuthorizationEndpoint extends AuthorizationEndpointBase
updateAuthenticationSession(deviceCodeModel);
// Verification OK
authenticationSession.setClientNote(OIDCLoginProtocol.OAUTH2_DEVICE_VERIFIED_USER_CODE, userCode);
authenticationSession.setClientNote(OIDCLoginProtocol.OAUTH2_DEVICE_VERIFIED_USER_CODE, formattedUserCode);
// Event logging for the verification
event.client(deviceCodeModel.getClientId())