From 5edf14944e6f3f23aac3b10ad3a0c18917d0c1a8 Mon Sep 17 00:00:00 2001 From: Hiroyuki Wada Date: Sun, 12 May 2019 15:47:15 +0900 Subject: [PATCH] KEYCLOAK-7675 SPI and default implementation for Device User Code. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Author: Hiroyuki Wada Date: Sun May 12 15:47:15 2019 +0900 Signed-off-by: Ɓukasz Dywicki --- .../DefaultOAuth2DeviceUserCodeProvider.java | 56 +++++++++++++++++++ ...ltOAuth2DeviceUserCodeProviderFactory.java | 51 +++++++++++++++++ .../models/OAuth2DeviceUserCodeProvider.java | 50 +++++++++++++++++ .../OAuth2DeviceUserCodeProviderFactory.java | 26 +++++++++ .../models/OAuth2DeviceUserCodeSpi.java | 50 +++++++++++++++++ ...models.OAuth2DeviceUserCodeProviderFactory | 18 ++++++ .../services/org.keycloak.provider.Spi | 1 + .../OAuth2DeviceAuthorizationEndpoint.java | 20 ++++--- 8 files changed, 263 insertions(+), 9 deletions(-) create mode 100644 server-spi-private/src/main/java/org/keycloak/models/DefaultOAuth2DeviceUserCodeProvider.java create mode 100644 server-spi-private/src/main/java/org/keycloak/models/DefaultOAuth2DeviceUserCodeProviderFactory.java create mode 100644 server-spi-private/src/main/java/org/keycloak/models/OAuth2DeviceUserCodeProvider.java create mode 100644 server-spi-private/src/main/java/org/keycloak/models/OAuth2DeviceUserCodeProviderFactory.java create mode 100644 server-spi-private/src/main/java/org/keycloak/models/OAuth2DeviceUserCodeSpi.java create mode 100644 server-spi-private/src/main/resources/META-INF/services/org.keycloak.models.OAuth2DeviceUserCodeProviderFactory diff --git a/server-spi-private/src/main/java/org/keycloak/models/DefaultOAuth2DeviceUserCodeProvider.java b/server-spi-private/src/main/java/org/keycloak/models/DefaultOAuth2DeviceUserCodeProvider.java new file mode 100644 index 0000000000..34cc4c8ae6 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/DefaultOAuth2DeviceUserCodeProvider.java @@ -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 Hiroyuki Wada + */ +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() { + + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/DefaultOAuth2DeviceUserCodeProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/models/DefaultOAuth2DeviceUserCodeProviderFactory.java new file mode 100644 index 0000000000..618583fa9e --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/DefaultOAuth2DeviceUserCodeProviderFactory.java @@ -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 Hiroyuki Wada + */ +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"; + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/OAuth2DeviceUserCodeProvider.java b/server-spi-private/src/main/java/org/keycloak/models/OAuth2DeviceUserCodeProvider.java new file mode 100644 index 0000000000..c1c4ded6fb --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/OAuth2DeviceUserCodeProvider.java @@ -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 Hiroyuki Wada + */ +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); +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/OAuth2DeviceUserCodeProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/models/OAuth2DeviceUserCodeProviderFactory.java new file mode 100644 index 0000000000..0f797bc287 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/OAuth2DeviceUserCodeProviderFactory.java @@ -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 Hiroyuki Wada + */ +public interface OAuth2DeviceUserCodeProviderFactory extends ProviderFactory { +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/OAuth2DeviceUserCodeSpi.java b/server-spi-private/src/main/java/org/keycloak/models/OAuth2DeviceUserCodeSpi.java new file mode 100644 index 0000000000..f060e6a40a --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/OAuth2DeviceUserCodeSpi.java @@ -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 Hiroyuki Wada + */ +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 getProviderClass(){ + return OAuth2DeviceUserCodeProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return OAuth2DeviceUserCodeProviderFactory.class; + } +} diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.models.OAuth2DeviceUserCodeProviderFactory b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.models.OAuth2DeviceUserCodeProviderFactory new file mode 100644 index 0000000000..418120ff25 --- /dev/null +++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.models.OAuth2DeviceUserCodeProviderFactory @@ -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 \ No newline at end of file diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi index b14edc47dd..93ae250b08 100755 --- a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -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 diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/OAuth2DeviceAuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/OAuth2DeviceAuthorizationEndpoint.java index 10654e6b8c..02666913b2 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/OAuth2DeviceAuthorizationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/OAuth2DeviceAuthorizationEndpoint.java @@ -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())