From ffbb2df1f31f432ad24eca24db531886fedef7f3 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Mon, 9 Mar 2015 09:40:41 +0100 Subject: [PATCH] KEYCLOAK-571 OpenID Connect Discovery KEYCLOAK-1091 JSON Web Key Set endpoint KEYCLOAK-790 One OpenID Connect token endpoint URL --- core/pom.xml | 6 + .../java/org/keycloak/OAuth2Constants.java | 4 + .../main/java/org/keycloak/jose/jwk/JWK.java | 62 + .../org/keycloak/jose/jwk/JWKBuilder.java | 79 ++ .../java/org/keycloak/jose/jwk/JWKParser.java | 54 + .../org/keycloak/jose/jwk/RSAPublicJWK.java | 38 + .../org/keycloak/util/JsonSerialization.java | 1 + .../org/keycloak/jose/jwk/JWKBuilderTest.java | 70 ++ .../main/java/org/keycloak/events/Errors.java | 2 + .../keycloak/protocol/saml/SamlService.java | 5 +- .../protocol/oidc/OIDCLoginProtocol.java | 4 + .../oidc/OIDCLoginProtocolService.java | 1102 ++--------------- .../protocol/oidc/OIDCWellKnownProvider.java | 70 ++ .../oidc/OIDCWellKnownProviderFactory.java | 40 + .../oidc/endpoints/AuthorizationEndpoint.java | 321 +++++ .../endpoints/LoginStatusIframeEndpoint.java | 91 ++ .../oidc/endpoints/LogoutEndpoint.java | 166 +++ .../oidc/endpoints/TokenEndpoint.java | 350 ++++++ .../UserInfoEndpoint.java} | 86 +- .../oidc/endpoints/ValidateTokenEndpoint.java | 103 ++ .../oidc/representations/JSONWebKeySet.java | 22 + .../OIDCConfigurationRepresentation.java | 123 ++ .../oidc/utils/AuthorizeClientUtil.java | 76 ++ .../protocol/oidc/utils/RedirectUtils.java | 134 ++ .../keycloak/services/ErrorPageException.java | 33 + .../services/ErrorResponseException.java | 39 + .../services/resources/AccountService.java | 5 +- .../resources/ClientsManagementService.java | 3 +- .../services/resources/RealmsResource.java | 17 +- .../resources/admin/UsersResource.java | 3 +- .../keycloak/wellknown/WellKnownProvider.java | 16 + .../wellknown/WellKnownProviderFactory.java | 10 + .../org/keycloak/wellknown/WellKnownSpi.java | 27 + .../services/org.keycloak.provider.Spi | 3 +- ...eycloak.wellknown.WellKnownProviderFactory | 1 + .../adapter/AdapterTestStrategy.java | 2 +- .../testsuite/oauth/AccessTokenTest.java | 16 +- .../testsuite/oauth/OAuthRedirectUriTest.java | 10 +- ...urceOwnerPasswordCredentialsGrantTest.java | 4 +- 39 files changed, 2121 insertions(+), 1077 deletions(-) create mode 100644 core/src/main/java/org/keycloak/jose/jwk/JWK.java create mode 100644 core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java create mode 100644 core/src/main/java/org/keycloak/jose/jwk/JWKParser.java create mode 100644 core/src/main/java/org/keycloak/jose/jwk/RSAPublicJWK.java create mode 100644 core/src/test/java/org/keycloak/jose/jwk/JWKBuilderTest.java create mode 100644 services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java create mode 100644 services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProviderFactory.java create mode 100644 services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java create mode 100644 services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java create mode 100644 services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java create mode 100644 services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java rename services/src/main/java/org/keycloak/protocol/oidc/{UserInfoService.java => endpoints/UserInfoEndpoint.java} (55%) create mode 100644 services/src/main/java/org/keycloak/protocol/oidc/endpoints/ValidateTokenEndpoint.java create mode 100644 services/src/main/java/org/keycloak/protocol/oidc/representations/JSONWebKeySet.java create mode 100644 services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java create mode 100644 services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java create mode 100644 services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java create mode 100644 services/src/main/java/org/keycloak/services/ErrorPageException.java create mode 100644 services/src/main/java/org/keycloak/services/ErrorResponseException.java create mode 100755 services/src/main/java/org/keycloak/wellknown/WellKnownProvider.java create mode 100755 services/src/main/java/org/keycloak/wellknown/WellKnownProviderFactory.java create mode 100755 services/src/main/java/org/keycloak/wellknown/WellKnownSpi.java create mode 100644 services/src/main/resources/META-INF/services/org.keycloak.wellknown.WellKnownProviderFactory diff --git a/core/pom.xml b/core/pom.xml index 5754ce3d84..3efc75aca7 100755 --- a/core/pom.xml +++ b/core/pom.xml @@ -56,6 +56,12 @@ junit test + + com.nimbusds + nimbus-jose-jwt + 3.9 + test + diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java index 07071ffa63..5aba901df5 100644 --- a/core/src/main/java/org/keycloak/OAuth2Constants.java +++ b/core/src/main/java/org/keycloak/OAuth2Constants.java @@ -25,6 +25,10 @@ public interface OAuth2Constants { String REFRESH_TOKEN = "refresh_token"; + String AUTHORIZATION_CODE = "authorization_code"; + + String PASSWORD = "password"; + } diff --git a/core/src/main/java/org/keycloak/jose/jwk/JWK.java b/core/src/main/java/org/keycloak/jose/jwk/JWK.java new file mode 100644 index 0000000000..d292f41977 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwk/JWK.java @@ -0,0 +1,62 @@ +package org.keycloak.jose.jwk; + +import org.codehaus.jackson.annotate.JsonProperty; + +/** + * @author Stian Thorgersen + */ +public class JWK { + + public static final String KEY_ID = "kid"; + + public static final String KEY_TYPE = "kty"; + + public static final String ALGORITHM = "alg"; + + public static final String PUBLIC_KEY_USE = "use"; + + @JsonProperty(KEY_ID) + private String keyId; + + @JsonProperty(KEY_TYPE) + private String keyType; + + @JsonProperty(ALGORITHM) + private String algorithm; + + @JsonProperty(PUBLIC_KEY_USE) + private String publicKeyUse; + + public String getKeyId() { + return keyId; + } + + public void setKeyId(String keyId) { + this.keyId = keyId; + } + + public String getKeyType() { + return keyType; + } + + public void setKeyType(String keyType) { + this.keyType = keyType; + } + + public String getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + public String getPublicKeyUse() { + return publicKeyUse; + } + + public void setPublicKeyUse(String publicKeyUse) { + this.publicKeyUse = publicKeyUse; + } + +} diff --git a/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java b/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java new file mode 100644 index 0000000000..bc3a228e51 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java @@ -0,0 +1,79 @@ +package org.keycloak.jose.jwk; + +import org.keycloak.util.Base64Url; + +import java.math.BigInteger; +import java.security.Key; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +/** + * @author Stian Thorgersen + */ +public class JWKBuilder { + + public static final String DEFAULT_PUBLIC_KEY_USE = "sig"; + public static final String DEFAULT_MESSAGE_DIGEST = "SHA-256"; + + + private JWKBuilder() { + } + + public static JWKBuilder create() { + return new JWKBuilder(); + } + + public JWK rs256(PublicKey key) { + RSAPublicKey rsaKey = (RSAPublicKey) key; + + RSAPublicJWK k = new RSAPublicJWK(); + k.setKeyId(createKeyId(key)); + k.setKeyType(RSAPublicJWK.RSA); + k.setAlgorithm(RSAPublicJWK.RS256); + k.setPublicKeyUse(DEFAULT_PUBLIC_KEY_USE); + k.setModulus(Base64Url.encode(toIntegerBytes(rsaKey.getModulus()))); + k.setPublicExponent(Base64Url.encode(toIntegerBytes(rsaKey.getPublicExponent()))); + + return k; + } + + private String createKeyId(Key key) { + try { + return Base64Url.encode(MessageDigest.getInstance(DEFAULT_MESSAGE_DIGEST).digest(key.getEncoded())); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + /** + * Copied from org.apache.commons.codec.binary.Base64 + */ + private static byte[] toIntegerBytes(final BigInteger bigInt) { + int bitlen = bigInt.bitLength(); + // round bitlen + bitlen = ((bitlen + 7) >> 3) << 3; + final byte[] bigBytes = bigInt.toByteArray(); + + if (((bigInt.bitLength() % 8) != 0) && (((bigInt.bitLength() / 8) + 1) == (bitlen / 8))) { + return bigBytes; + } + // set up params for copying everything but sign bit + int startSrc = 0; + int len = bigBytes.length; + + // if bigInt is exactly byte-aligned, just skip signbit in copy + if ((bigInt.bitLength() % 8) == 0) { + startSrc = 1; + len--; + } + final int startDst = bitlen / 8 - len; // to pad w/ nulls as per spec + final byte[] resizedBytes = new byte[bitlen / 8]; + System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len); + return resizedBytes; + } + +} diff --git a/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java b/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java new file mode 100644 index 0000000000..38f02d83cb --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java @@ -0,0 +1,54 @@ +package org.keycloak.jose.jwk; + +import org.codehaus.jackson.type.TypeReference; +import org.keycloak.util.Base64Url; +import org.keycloak.util.JsonSerialization; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.spec.RSAPublicKeySpec; +import java.util.Map; + +/** + * @author Stian Thorgersen + */ +public class JWKParser { + + private static TypeReference> typeRef = new TypeReference>() {}; + + private Map values; + + private JWKParser() { + } + + public static JWKParser create() { + return new JWKParser(); + } + + public JWKParser parse(String jwk) { + try { + this.values = JsonSerialization.mapper.readValue(jwk, typeRef); + return this; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public PublicKey toPublicKey() { + String algorithm = values.get(JWK.KEY_TYPE); + if (RSAPublicJWK.RSA.equals(algorithm)) { + BigInteger modulus = new BigInteger(1, Base64Url.decode(values.get(RSAPublicJWK.MODULUS))); + BigInteger publicExponent = new BigInteger(1, Base64Url.decode(values.get(RSAPublicJWK.PUBLIC_EXPONENT))); + + try { + return KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(modulus, publicExponent)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + throw new RuntimeException("Unsupported algorithm " + algorithm); + } + } + +} diff --git a/core/src/main/java/org/keycloak/jose/jwk/RSAPublicJWK.java b/core/src/main/java/org/keycloak/jose/jwk/RSAPublicJWK.java new file mode 100644 index 0000000000..8090599865 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwk/RSAPublicJWK.java @@ -0,0 +1,38 @@ +package org.keycloak.jose.jwk; + +import org.codehaus.jackson.annotate.JsonProperty; + +/** + * @author Stian Thorgersen + */ +public class RSAPublicJWK extends JWK { + + public static final String RSA = "RSA"; + public static final String RS256 = "RS256"; + + public static final String MODULUS = "n"; + public static final String PUBLIC_EXPONENT = "e"; + + @JsonProperty(MODULUS) + private String modulus; + + @JsonProperty("e") + private String publicExponent; + + public String getModulus() { + return modulus; + } + + public void setModulus(String modulus) { + this.modulus = modulus; + } + + public String getPublicExponent() { + return publicExponent; + } + + public void setPublicExponent(String publicExponent) { + this.publicExponent = publicExponent; + } + +} diff --git a/core/src/main/java/org/keycloak/util/JsonSerialization.java b/core/src/main/java/org/keycloak/util/JsonSerialization.java index a1a93ba1cc..ff080def36 100755 --- a/core/src/main/java/org/keycloak/util/JsonSerialization.java +++ b/core/src/main/java/org/keycloak/util/JsonSerialization.java @@ -3,6 +3,7 @@ package org.keycloak.util; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.SerializationConfig; import org.codehaus.jackson.map.annotate.JsonSerialize; +import org.codehaus.jackson.type.TypeReference; import java.io.IOException; import java.io.InputStream; diff --git a/core/src/test/java/org/keycloak/jose/jwk/JWKBuilderTest.java b/core/src/test/java/org/keycloak/jose/jwk/JWKBuilderTest.java new file mode 100644 index 0000000000..7b6a8612b8 --- /dev/null +++ b/core/src/test/java/org/keycloak/jose/jwk/JWKBuilderTest.java @@ -0,0 +1,70 @@ +package org.keycloak.jose.jwk; + +import com.nimbusds.jose.jwk.RSAKey; +import org.junit.Test; +import org.keycloak.jose.jws.Algorithm; +import org.keycloak.util.Base64Url; +import org.keycloak.util.JsonSerialization; +import sun.security.rsa.RSAPublicKeyImpl; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * @author Stian Thorgersen + */ +public class JWKBuilderTest { + + @Test + public void publicRs256() throws Exception { + PublicKey publicKey = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic(); + + JWK jwk = JWKBuilder.create().rs256(publicKey); + + assertNotNull(jwk.getKeyId()); + assertEquals("RSA", jwk.getKeyType()); + assertEquals("RS256", jwk.getAlgorithm()); + assertEquals("sig", jwk.getPublicKeyUse()); + + assertTrue(jwk instanceof RSAPublicJWK); + assertNotNull(((RSAPublicJWK) jwk).getModulus()); + assertNotNull(((RSAPublicJWK) jwk).getPublicExponent()); + + String jwkJson = JsonSerialization.writeValueAsString(jwk); + + // Parse + assertArrayEquals(publicKey.getEncoded(), JWKParser.create().parse(jwkJson).toPublicKey().getEncoded()); + + // Parse with 3rd party lib + assertArrayEquals(publicKey.getEncoded(), RSAKey.parse(jwkJson).toRSAPublicKey().getEncoded()); + } + + @Test + public void parse() throws NoSuchAlgorithmException, InvalidKeySpecException { + String jwkJson = "{" + + " \"kty\": \"RSA\"," + + " \"alg\": \"RS256\"," + + " \"use\": \"sig\"," + + " \"kid\": \"3121adaa80ace09f89d80899d4a5dc4ce33d0747\"," + + " \"n\": \"soFDjoZ5mQ8XAA7reQAFg90inKAHk0DXMTizo4JuOsgzUbhcplIeZ7ks83hsEjm8mP8lUVaHMPMAHEIp3gu6Xxsg-s73ofx1dtt_Fo7aj8j383MFQGl8-FvixTVobNeGeC0XBBQjN8lEl-lIwOa4ZoERNAShplTej0ntDp7TQm0=\"," + + " \"e\": \"AQAB\"" + + " }"; + + PublicKey key = JWKParser.create().parse(jwkJson).toPublicKey(); + assertEquals("RSA", key.getAlgorithm()); + assertEquals("X.509", key.getFormat()); + } + +} diff --git a/events/api/src/main/java/org/keycloak/events/Errors.java b/events/api/src/main/java/org/keycloak/events/Errors.java index f6531ebcaa..7a4404db7f 100755 --- a/events/api/src/main/java/org/keycloak/events/Errors.java +++ b/events/api/src/main/java/org/keycloak/events/Errors.java @@ -5,6 +5,8 @@ package org.keycloak.events; */ public interface Errors { + String INVALID_REQUEST = "invalid_request"; + String REALM_DISABLED = "realm_disabled"; String CLIENT_NOT_FOUND = "client_not_found"; diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java index 080b7e8444..a6a2330270 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java @@ -19,6 +19,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; +import org.keycloak.protocol.oidc.utils.RedirectUtils; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.HttpAuthenticationManager; @@ -236,7 +237,7 @@ public class SamlService { String redirect = null; URI redirectUri = requestAbstractType.getAssertionConsumerServiceURL(); if (redirectUri != null && !"null".equals(redirectUri)) { // "null" is for testing purposes - redirect = OIDCLoginProtocolService.verifyRedirectUri(uriInfo, redirectUri.toString(), realm, client); + redirect = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri.toString(), realm, client); } else { if (bindingType.equals(SamlProtocol.SAML_POST_BINDING)) { redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE); @@ -376,7 +377,7 @@ public class SamlService { } if (redirectUri != null) { - redirectUri = OIDCLoginProtocolService.verifyRedirectUri(uriInfo, redirectUri, realm, client); + redirectUri = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri, realm, client); if (redirectUri == null) { return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect uri."); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java index 9258264b9b..3900f1aaf3 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java @@ -46,11 +46,15 @@ public class OIDCLoginProtocol implements LoginProtocol { public static final String LOGIN_PROTOCOL = "openid-connect"; public static final String STATE_PARAM = "state"; public static final String SCOPE_PARAM = "scope"; + public static final String CODE_PARAM = "code"; public static final String RESPONSE_TYPE_PARAM = "response_type"; + public static final String GRANT_TYPE_PARAM = "grant_type"; public static final String REDIRECT_URI_PARAM = "redirect_uri"; public static final String CLIENT_ID_PARAM = "client_id"; public static final String PROMPT_PARAM = "prompt"; public static final String LOGIN_HINT_PARAM = "login_hint"; + public static final String K_IDP_HINT = "k_idp_hint"; + private static final Logger log = Logger.getLogger(OIDCLoginProtocol.class); protected KeycloakSession session; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java index 3de9a8716a..c2ec74bb4b 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java @@ -2,83 +2,49 @@ package org.keycloak.protocol.oidc; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; -import org.jboss.resteasy.spi.BadRequestException; import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpResponse; -import org.jboss.resteasy.spi.NotAcceptableException; -import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; -import org.jboss.resteasy.spi.UnauthorizedException; import org.keycloak.ClientConnection; -import org.keycloak.Config; import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; import org.keycloak.RSATokenVerifier; -import org.keycloak.constants.AdapterConstants; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; +import org.keycloak.jose.jwk.JWK; +import org.keycloak.jose.jwk.JWKBuilder; import org.keycloak.login.LoginFormsProvider; -import org.keycloak.models.ApplicationModel; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.Constants; -import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.OAuthClientModel; import org.keycloak.models.RealmModel; -import org.keycloak.models.RequiredCredentialModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; -import org.keycloak.models.UserSessionProvider; -import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint; +import org.keycloak.protocol.oidc.endpoints.LoginStatusIframeEndpoint; +import org.keycloak.protocol.oidc.endpoints.LogoutEndpoint; +import org.keycloak.protocol.oidc.endpoints.TokenEndpoint; +import org.keycloak.protocol.oidc.endpoints.UserInfoEndpoint; +import org.keycloak.protocol.oidc.endpoints.ValidateTokenEndpoint; +import org.keycloak.protocol.oidc.representations.JSONWebKeySet; import org.keycloak.representations.AccessToken; -import org.keycloak.representations.AccessTokenResponse; -import org.keycloak.representations.RefreshToken; -import org.keycloak.services.ForbiddenException; +import org.keycloak.services.ErrorResponseException; import org.keycloak.services.managers.AuthenticationManager; -import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus; -import org.keycloak.services.managers.ClientSessionCode; -import org.keycloak.services.managers.HttpAuthenticationManager; -import org.keycloak.services.resources.Cors; import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.flows.Flows; -import org.keycloak.services.resources.flows.Urls; -import org.keycloak.util.BasicAuthHelper; -import org.keycloak.util.StreamUtil; -import org.keycloak.util.UriUtils; -import javax.ws.rs.Consumes; import javax.ws.rs.GET; -import javax.ws.rs.HeaderParam; -import javax.ws.rs.OPTIONS; -import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; -import javax.ws.rs.core.CacheControl; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.Providers; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; import java.util.HashMap; -import java.util.HashSet; -import java.util.List; import java.util.Map; -import java.util.Set; - -import static org.keycloak.constants.AdapterConstants.K_IDP_HINT; /** * Resource class for the oauth/openid connect token service @@ -90,32 +56,16 @@ public class OIDCLoginProtocolService { protected static final Logger logger = Logger.getLogger(OIDCLoginProtocolService.class); - protected RealmModel realm; - protected TokenManager tokenManager; + private RealmModel realm; + private TokenManager tokenManager; private EventBuilder event; - protected AuthenticationManager authManager; + private AuthenticationManager authManager; @Context - protected Providers providers; - @Context - protected SecurityContext securityContext; - @Context - protected UriInfo uriInfo; - @Context - protected HttpHeaders headers; - @Context - protected HttpRequest request; - @Context - protected HttpResponse response; - @Context - protected KeycloakSession session; - @Context - protected ClientConnection clientConnection; + private UriInfo uriInfo; - /* @Context - protected ResourceContext resourceContext; - */ + private KeycloakSession session; public OIDCLoginProtocolService(RealmModel realm, EventBuilder event, AuthenticationManager authManager) { this.realm = realm; @@ -133,12 +83,6 @@ public class OIDCLoginProtocolService { return baseUriBuilder.path(RealmsResource.class).path("{realm}/protocol/" + OIDCLoginProtocol.LOGIN_PROTOCOL); } - public static UriBuilder accessCodeToTokenUrl(UriInfo uriInfo) { - UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder(); - return accessCodeToTokenUrl(baseUriBuilder); - - } - public static UriBuilder accessCodeToTokenUrl(UriBuilder baseUriBuilder) { UriBuilder uriBuilder = tokenServiceBaseUrl(baseUriBuilder); return uriBuilder.path(OIDCLoginProtocolService.class, "accessCodeToToken"); @@ -149,12 +93,6 @@ public class OIDCLoginProtocolService { return uriBuilder.path(OIDCLoginProtocolService.class, "validateAccessToken"); } - public static UriBuilder grantAccessTokenUrl(UriInfo uriInfo) { - UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder(); - return grantAccessTokenUrl(baseUriBuilder); - - } - public static UriBuilder grantAccessTokenUrl(UriBuilder baseUriBuilder) { UriBuilder uriBuilder = tokenServiceBaseUrl(baseUriBuilder); return uriBuilder.path(OIDCLoginProtocolService.class, "grantAccessToken"); @@ -186,811 +124,103 @@ public class OIDCLoginProtocolService { } /** - * - * - * @param client_id - * @param origin - * @return + * Authorization endpoint */ + @Path("auth") + public Object auth() { + AuthorizationEndpoint endpoint = new AuthorizationEndpoint(authManager, realm, event); + ResteasyProviderFactory.getInstance().injectProperties(endpoint); + return endpoint.init(); + } + + /** + * Registration endpoint + */ + @Path("registrations") + public Object registerPage() { + AuthorizationEndpoint endpoint = new AuthorizationEndpoint(authManager, realm, event); + ResteasyProviderFactory.getInstance().injectProperties(endpoint); + return endpoint.register().init(); + } + + /** + * Token endpoint + */ + @Path("token") + public Object token() { + TokenEndpoint endpoint = new TokenEndpoint(tokenManager, authManager, realm, event); + ResteasyProviderFactory.getInstance().injectProperties(endpoint); + return endpoint.init(); + } + + @Path("login") + @Deprecated + public Object loginPage() { + AuthorizationEndpoint endpoint = new AuthorizationEndpoint(authManager, realm, event); + ResteasyProviderFactory.getInstance().injectProperties(endpoint); + return endpoint.legacy(OIDCLoginProtocol.CODE_PARAM).init(); + } + @Path("login-status-iframe.html") - @GET - @Produces(MediaType.TEXT_HTML) - public Response getLoginStatusIframe(@QueryParam("client_id") String client_id, - @QueryParam("origin") String origin) { - if (!UriUtils.isOrigin(origin)) { - throw new BadRequestException("Invalid origin"); - } - - ClientModel client = realm.findClient(client_id); - if (client == null) { - throw new NotFoundException("could not find client"); - } - - InputStream is = getClass().getClassLoader().getResourceAsStream("login-status-iframe.html"); - if (is == null) throw new NotFoundException("Could not find login-status-iframe.html "); - - boolean valid = false; - for (String o : client.getWebOrigins()) { - if (o.equals("*") || o.equals(origin)) { - valid = true; - break; - } - } - - for (String r : OIDCLoginProtocolService.resolveValidRedirects(uriInfo, client.getRedirectUris())) { - int i = r.indexOf('/', 8); - if (i != -1) { - r = r.substring(0, i); - } - - if (r.equals(origin)) { - valid = true; - break; - } - } - - if (!valid) { - throw new BadRequestException("Invalid origin"); - } - - try { - String file = StreamUtil.readString(is); - file = file.replace("ORIGIN", origin); - - CacheControl cacheControl = new CacheControl(); - cacheControl.setNoTransform(false); - cacheControl.setMaxAge(Config.scope("theme").getInt("staticMaxAge", -1)); - - return Response.ok(file).cacheControl(cacheControl).build(); - } catch (IOException e) { - throw new RuntimeException(e); - } + public Object getLoginStatusIframe() { + LoginStatusIframeEndpoint endpoint = new LoginStatusIframeEndpoint(realm); + ResteasyProviderFactory.getInstance().injectProperties(endpoint); + return endpoint; } - - /** - * Direct grant REST invocation. One stop call to obtain an access token. - * - * If the client is a confidential client - * you must include the client-id (application name or oauth client name) and secret in an Basic Auth Authorization header. - * - * If the client is a public client, then you must include a "client_id" form parameter with the app's or oauth client's name. - * - * The realm must be configured to allow these types of auth requests. (Direct Grant API in admin console Settings page) - * - * - * @See http://tools.ietf.org/html/rfc6749#section-4.3 - * - * @param authorizationHeader - * @param form - * @return @see org.keycloak.representations.AccessTokenResponse - */ @Path("grants/access") - @POST - @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - @Produces(MediaType.APPLICATION_JSON) - public Response grantAccessToken(final @HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader, - final MultivaluedMap form) { - if (!checkSsl()) { - return createError("https_required", "HTTPS required", Response.Status.FORBIDDEN); - } - - if (!realm.isPasswordCredentialGrantAllowed()) { - return createError("not_enabled", "Direct Grant REST API not enabled", Response.Status.FORBIDDEN); - } - - event.event(EventType.LOGIN).detail(Details.AUTH_METHOD, "oauth_credentials").detail(Details.RESPONSE_TYPE, "token"); - - String username = form.getFirst(AuthenticationManager.FORM_USERNAME); - if (username == null) { - event.error(Errors.USERNAME_MISSING); - throw new UnauthorizedException("No username"); - } - event.detail(Details.USERNAME, username); - - UserModel user = KeycloakModelUtils.findUserByNameOrEmail(session, realm, username); - if (user != null) event.user(user); - - ClientModel client = authorizeClient(authorizationHeader, form, event); - - if (!realm.isEnabled()) { - event.error(Errors.REALM_DISABLED); - return createError("realm_disabled", "Realm is disabled", Response.Status.UNAUTHORIZED); - } - - AuthenticationStatus authenticationStatus = authManager.authenticateForm(session, clientConnection, realm, form); - Map err; - - switch (authenticationStatus) { - case SUCCESS: - break; - case ACCOUNT_TEMPORARILY_DISABLED: - case ACTIONS_REQUIRED: - err = new HashMap(); - err.put(OAuth2Constants.ERROR, "invalid_grant"); - err.put(OAuth2Constants.ERROR_DESCRIPTION, "AccountProvider temporarily disabled"); - event.error(Errors.USER_TEMPORARILY_DISABLED); - return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err) - .build(); - case ACCOUNT_DISABLED: - err = new HashMap(); - err.put(OAuth2Constants.ERROR, "invalid_grant"); - err.put(OAuth2Constants.ERROR_DESCRIPTION, "AccountProvider disabled"); - event.error(Errors.USER_DISABLED); - return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err) - .build(); - default: - err = new HashMap(); - err.put(OAuth2Constants.ERROR, "invalid_grant"); - err.put(OAuth2Constants.ERROR_DESCRIPTION, "Invalid user credentials"); - event.error(Errors.INVALID_USER_CREDENTIALS); - return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err) - .build(); - } - - String scope = form.getFirst(OAuth2Constants.SCOPE); - - UserSessionProvider sessions = session.sessions(); - - UserSessionModel userSession = sessions.createUserSession(realm, user, username, clientConnection.getRemoteAddr(), "oauth_credentials", false); - event.session(userSession); - - ClientSessionModel clientSession = sessions.createClientSession(realm, client); - clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL); - - TokenManager.attachClientSession(userSession, clientSession); - - AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession) - .generateAccessToken(session, scope, client, user, userSession, clientSession) - .generateRefreshToken() - .generateIDToken() - .build(); - - event.success(); - - return Response.ok(res, MediaType.APPLICATION_JSON_TYPE).build(); + @Deprecated + public Object grantAccessToken() { + TokenEndpoint endpoint = new TokenEndpoint(tokenManager, authManager, realm, event); + ResteasyProviderFactory.getInstance().injectProperties(endpoint); + return endpoint.legacy(OAuth2Constants.PASSWORD).init(); + } + + @Path("refresh") + @Deprecated + public Object refreshAccessToken() { + TokenEndpoint endpoint = new TokenEndpoint(tokenManager, authManager, realm, event); + ResteasyProviderFactory.getInstance().injectProperties(endpoint); + return endpoint.legacy(OAuth2Constants.REFRESH_TOKEN).init(); + } + + @Path("access/codes") + @Deprecated + public Object accessCodeToToken() { + TokenEndpoint endpoint = new TokenEndpoint(tokenManager, authManager, realm, event); + ResteasyProviderFactory.getInstance().injectProperties(endpoint); + return endpoint.legacy(OAuth2Constants.AUTHORIZATION_CODE).init(); } - /** - * Validate encoded access token. - * - * @param tokenString - * @return Unmarshalled token - */ @Path("validate") + public Object validateAccessToken(@QueryParam("access_token") String tokenString) { + ValidateTokenEndpoint endpoint = new ValidateTokenEndpoint(tokenManager, realm, event); + ResteasyProviderFactory.getInstance().injectProperties(endpoint); + return endpoint; + + } + @GET - @NoCache + @Path("certs") @Produces(MediaType.APPLICATION_JSON) - public Response validateAccessToken(@QueryParam("access_token") String tokenString) { - if (!checkSsl()) { - return createError("https_required", "HTTPS required", Response.Status.FORBIDDEN); - } - event.event(EventType.VALIDATE_ACCESS_TOKEN); - AccessToken token = null; - try { - token = RSATokenVerifier.verifyToken(tokenString, realm.getPublicKey(), realm.getName()); - } catch (Exception e) { - Map err = new HashMap(); - err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_GRANT); - err.put(OAuth2Constants.ERROR_DESCRIPTION, "Token invalid"); - logger.error("Invalid token. Token verification failed."); - event.error(Errors.INVALID_TOKEN); - return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err) - .build(); - } - event.user(token.getSubject()).session(token.getSessionState()).detail(Details.VALIDATE_ACCESS_TOKEN, token.getId()); - - try { - tokenManager.validateToken(session, uriInfo, clientConnection, realm, token); - } catch (OAuthErrorException e) { - Map error = new HashMap(); - error.put(OAuth2Constants.ERROR, e.getError()); - if (e.getDescription() != null) error.put(OAuth2Constants.ERROR_DESCRIPTION, e.getDescription()); - event.error(Errors.INVALID_TOKEN); - return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build(); - } - event.success(); - - return Response.ok(token, MediaType.APPLICATION_JSON_TYPE).build(); - } - - /** - * CORS preflight path for refresh token requests - * - * @return - */ - @Path("refresh") - @OPTIONS - @Produces(MediaType.APPLICATION_JSON) - public Response refreshAccessTokenPreflight() { - if (logger.isDebugEnabled()) { - logger.debugv("cors request from: {0}", request.getHttpHeaders().getRequestHeaders().getFirst("Origin")); - } - return Cors.add(request, Response.ok()).auth().preflight().build(); - } - - /** - * URL for making refresh token requests. - * - * @See http://tools.ietf.org/html/rfc6749#section-6 - * - * If the client is a confidential client - * you must include the client-id (application name or oauth client name) and secret in an Basic Auth Authorization header. - * - * If the client is a public client, then you must include a "client_id" form parameter with the app's or oauth client's name. - * - * @param authorizationHeader - * @param form - * @return - */ - @Path("refresh") - @POST - @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - @Produces(MediaType.APPLICATION_JSON) - public Response refreshAccessToken(final @HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader, - final MultivaluedMap form) { - if (!checkSsl()) { - return createError("https_required", "HTTPS required", Response.Status.FORBIDDEN); - } - - event.event(EventType.REFRESH_TOKEN); - - ClientModel client = authorizeClient(authorizationHeader, form, event); - String refreshToken = form.getFirst(OAuth2Constants.REFRESH_TOKEN); - if (refreshToken == null) { - Map error = new HashMap(); - error.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_REQUEST); - error.put(OAuth2Constants.ERROR_DESCRIPTION, "No refresh token"); - event.error(Errors.INVALID_TOKEN); - return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build(); - } - AccessTokenResponse res; - try { - res = tokenManager.refreshAccessToken(session, uriInfo, clientConnection, realm, client, refreshToken, event); - } catch (OAuthErrorException e) { - Map error = new HashMap(); - error.put(OAuth2Constants.ERROR, e.getError()); - if (e.getDescription() != null) error.put(OAuth2Constants.ERROR_DESCRIPTION, e.getDescription()); - event.error(Errors.INVALID_TOKEN); - return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build(); - } - - - event.success(); - - return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build(); - } - - /** - * CORS preflight path for access code to token - * - * @return - */ - @Path("access/codes") - @OPTIONS - @Produces("application/json") - public Response accessCodeToTokenPreflight() { - if (logger.isDebugEnabled()) { - logger.debugv("cors request from: {0}", request.getHttpHeaders().getRequestHeaders().getFirst("Origin")); - } - return Cors.add(request, Response.ok()).auth().preflight().build(); - } - - /** - * URL invoked by adapter to turn an access code to access token - * - * @See http://tools.ietf.org/html/rfc6749#section-4.1 - * - * @param authorizationHeader - * @param formData - * @return - */ - @Path("access/codes") - @POST - @Produces("application/json") - public Response accessCodeToToken(@HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader, final MultivaluedMap formData) { - if (!checkSsl()) { - throw new ForbiddenException("HTTPS required"); - } - - event.event(EventType.CODE_TO_TOKEN); - - if (!realm.isEnabled()) { - event.error(Errors.REALM_DISABLED); - throw new UnauthorizedException("Realm not enabled"); - } - - String code = formData.getFirst(OAuth2Constants.CODE); - if (code == null) { - Map error = new HashMap(); - error.put(OAuth2Constants.ERROR, "invalid_request"); - error.put(OAuth2Constants.ERROR_DESCRIPTION, "code not specified"); - event.error(Errors.INVALID_CODE); - throw new BadRequestException("Code not specified", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); - } - ClientSessionCode accessCode = ClientSessionCode.parse(code, session, realm); - if (accessCode == null) { - String[] parts = code.split("\\."); - if (parts.length == 2) { - try { - event.detail(Details.CODE_ID, new String(parts[1])); - } catch (Throwable t) { - } - } - Map res = new HashMap(); - res.put(OAuth2Constants.ERROR, "invalid_grant"); - res.put(OAuth2Constants.ERROR_DESCRIPTION, "Code not found"); - event.error(Errors.INVALID_CODE); - return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res) - .build(); - } - - ClientSessionModel clientSession = accessCode.getClientSession(); - event.detail(Details.CODE_ID, clientSession.getId()); - if (!accessCode.isValid(ClientSessionModel.Action.CODE_TO_TOKEN)) { - Map res = new HashMap(); - res.put(OAuth2Constants.ERROR, "invalid_grant"); - res.put(OAuth2Constants.ERROR_DESCRIPTION, "Code is expired"); - event.error(Errors.INVALID_CODE); - return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res) - .build(); - } - - accessCode.setAction(null); - UserSessionModel userSession = clientSession.getUserSession(); - event.user(userSession.getUser()); - event.session(userSession.getId()); - - ClientModel client = authorizeClient(authorizationHeader, formData, event); - - String redirectUri = clientSession.getNote(OIDCLoginProtocol.REDIRECT_URI_PARAM); - if (redirectUri != null && !redirectUri.equals(formData.getFirst(OAuth2Constants.REDIRECT_URI))) { - Map res = new HashMap(); - res.put(OAuth2Constants.ERROR, "invalid_grant"); - res.put(OAuth2Constants.ERROR_DESCRIPTION, "Incorrect redirect_uri"); - event.error(Errors.INVALID_CODE); - return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res) - .build(); - } - - if (!client.getClientId().equals(clientSession.getClient().getClientId())) { - Map res = new HashMap(); - res.put(OAuth2Constants.ERROR, "invalid_grant"); - res.put(OAuth2Constants.ERROR_DESCRIPTION, "Auth error"); - event.error(Errors.INVALID_CODE); - return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res) - .build(); - } - - UserModel user = session.users().getUserById(userSession.getUser().getId(), realm); - if (user == null) { - Map res = new HashMap(); - res.put(OAuth2Constants.ERROR, "invalid_grant"); - res.put(OAuth2Constants.ERROR_DESCRIPTION, "User not found"); - event.error(Errors.INVALID_CODE); - return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res) - .build(); - } - - if (!user.isEnabled()) { - Map res = new HashMap(); - res.put(OAuth2Constants.ERROR, "invalid_grant"); - res.put(OAuth2Constants.ERROR_DESCRIPTION, "User disabled"); - event.error(Errors.INVALID_CODE); - return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res) - .build(); - } - - if (!AuthenticationManager.isSessionValid(realm, userSession)) { - AuthenticationManager.logout(session, realm, userSession, uriInfo, clientConnection); - Map res = new HashMap(); - res.put(OAuth2Constants.ERROR, "invalid_grant"); - res.put(OAuth2Constants.ERROR_DESCRIPTION, "Session not active"); - event.error(Errors.INVALID_CODE); - return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res) - .build(); - } - - String adapterSessionId = formData.getFirst(AdapterConstants.APPLICATION_SESSION_STATE); - if (adapterSessionId != null) { - String adapterSessionHost = formData.getFirst(AdapterConstants.APPLICATION_SESSION_HOST); - logger.debugf("Adapter Session '%s' saved in ClientSession for client '%s'. Host is '%s'", adapterSessionId, client.getClientId(), adapterSessionHost); - - event.detail(AdapterConstants.APPLICATION_SESSION_STATE, adapterSessionId); - clientSession.setNote(AdapterConstants.APPLICATION_SESSION_STATE, adapterSessionId); - event.detail(AdapterConstants.APPLICATION_SESSION_HOST, adapterSessionHost); - clientSession.setNote(AdapterConstants.APPLICATION_SESSION_HOST, adapterSessionHost); - } - - AccessToken token = tokenManager.createClientAccessToken(session, accessCode.getRequestedRoles(), realm, client, user, userSession, clientSession); - - AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession) - .accessToken(token) - .generateIDToken() - .generateRefreshToken().build(); - - event.success(); - - return Cors.add(request, Response.ok(res)).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build(); + public JSONWebKeySet certs() { + JSONWebKeySet keySet = new JSONWebKeySet(); + keySet.setKeys(new JWK[]{JWKBuilder.create().rs256(realm.getPublicKey())}); + return keySet; } @Path("userinfo") public Object issueUserInfo() { - UserInfoService userInfoEndpoint = new UserInfoService(this); - - ResteasyProviderFactory.getInstance().injectProperties(userInfoEndpoint); - - return userInfoEndpoint; + UserInfoEndpoint endpoint = new UserInfoEndpoint(tokenManager, realm); + ResteasyProviderFactory.getInstance().injectProperties(endpoint); + return endpoint; } - protected ClientModel authorizeClient(String authorizationHeader, MultivaluedMap formData, EventBuilder event) { - ClientModel client = authorizeClientBase(authorizationHeader, formData, event, realm); - - if ( (client instanceof ApplicationModel) && ((ApplicationModel)client).isBearerOnly()) { - Map error = new HashMap(); - error.put(OAuth2Constants.ERROR, "invalid_client"); - error.put(OAuth2Constants.ERROR_DESCRIPTION, "Bearer-only not allowed"); - event.error(Errors.INVALID_CLIENT); - throw new BadRequestException("Bearer-only not allowed", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); - } - - return client; - } - - // Just authorize client without further checking about client type - public static ClientModel authorizeClientBase(String authorizationHeader, MultivaluedMap formData, EventBuilder event, RealmModel realm) { - String client_id; - String clientSecret; - if (authorizationHeader != null) { - String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader); - if (usernameSecret == null) { - throw new UnauthorizedException("Bad Authorization header", Response.status(401).header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"" + realm.getName() + "\"").build()); - } - client_id = usernameSecret[0]; - clientSecret = usernameSecret[1]; - } else { - client_id = formData.getFirst(OAuth2Constants.CLIENT_ID); - clientSecret = formData.getFirst("client_secret"); - } - - if (client_id == null) { - Map error = new HashMap(); - error.put(OAuth2Constants.ERROR, "invalid_client"); - error.put(OAuth2Constants.ERROR_DESCRIPTION, "Could not find client"); - throw new BadRequestException("Could not find client", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); - } - - event.client(client_id); - - ClientModel client = realm.findClient(client_id); - if (client == null) { - Map error = new HashMap(); - error.put(OAuth2Constants.ERROR, "invalid_client"); - error.put(OAuth2Constants.ERROR_DESCRIPTION, "Could not find client"); - event.error(Errors.CLIENT_NOT_FOUND); - throw new BadRequestException("Could not find client", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); - } - - if (!client.isEnabled()) { - Map error = new HashMap(); - error.put(OAuth2Constants.ERROR, "invalid_client"); - error.put(OAuth2Constants.ERROR_DESCRIPTION, "Client is not enabled"); - event.error(Errors.CLIENT_DISABLED); - throw new BadRequestException("Client is not enabled", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); - } - - if (!client.isPublicClient()) { - if (clientSecret == null || !client.validateSecret(clientSecret)) { - Map error = new HashMap(); - error.put(OAuth2Constants.ERROR, "unauthorized_client"); - event.error(Errors.INVALID_CLIENT_CREDENTIALS); - throw new BadRequestException("Unauthorized Client", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); - } - } - - return client; - } - - /** - * checks input and initializes variables based on a front page action like the login page or registration page - * - */ - private class FrontPageInitializer { - protected String clientId; - protected String redirect; - protected String state; - protected String scopeParam; - protected String responseType; - protected String loginHint; - protected String prompt; - protected ClientSessionModel clientSession; - - public Response processInput() { - event.client(clientId).detail(Details.REDIRECT_URI, redirect).detail(Details.RESPONSE_TYPE, "code"); - if (!checkSsl()) { - event.error(Errors.SSL_REQUIRED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required"); - } - if (!realm.isEnabled()) { - event.error(Errors.REALM_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled"); - } - - clientSession = null; - ClientModel client = realm.findClient(clientId); - if (client == null) { - event.error(Errors.CLIENT_NOT_FOUND); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown login requester."); - } - - if (!client.isEnabled()) { - event.error(Errors.CLIENT_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Login requester not enabled."); - } - if ((client instanceof ApplicationModel) && ((ApplicationModel)client).isBearerOnly()) { - event.error(Errors.NOT_ALLOWED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Bearer-only applications are not allowed to initiate browser login"); - } - if (client.isDirectGrantsOnly()) { - event.error(Errors.NOT_ALLOWED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "direct-grants-only clients are not allowed to initiate browser login"); - } - String redirectUriParam = redirect; - redirect = verifyRedirectUri(uriInfo, redirect, realm, client); - if (redirect == null) { - event.error(Errors.INVALID_REDIRECT_URI); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect_uri."); - } - clientSession = session.sessions().createClientSession(realm, client); - clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL); - clientSession.setRedirectUri(redirect); - clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE); - clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret()); - clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state); - clientSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUriParam); - if (scopeParam != null) clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, scopeParam); - if (responseType != null) clientSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, responseType); - if (loginHint != null) clientSession.setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, loginHint); - if (prompt != null) clientSession.setNote(OIDCLoginProtocol.PROMPT_PARAM, prompt); - return null; - } - } - - /** - * Login page. Must be redirected to from the application or oauth client. - * - * @See http://tools.ietf.org/html/rfc6749#section-4.1 - * - * - * @param responseType - * @param redirect - * @param clientId - * @param scopeParam - * @param state - * @param prompt - * @return - */ - @Path("login") - @GET - public Response loginPage(@QueryParam(OIDCLoginProtocol.RESPONSE_TYPE_PARAM) String responseType, - @QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirect, - @QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId, - @QueryParam(OIDCLoginProtocol.SCOPE_PARAM) String scopeParam, - @QueryParam(OIDCLoginProtocol.STATE_PARAM) String state, - @QueryParam(OIDCLoginProtocol.PROMPT_PARAM) String prompt, - @QueryParam(OIDCLoginProtocol.LOGIN_HINT_PARAM) String loginHint, - @QueryParam(K_IDP_HINT) String idpHint) { - event.event(EventType.LOGIN); - FrontPageInitializer pageInitializer = new FrontPageInitializer(); - pageInitializer.responseType = responseType; - pageInitializer.redirect = redirect; - pageInitializer.clientId = clientId; - pageInitializer.scopeParam = scopeParam; - pageInitializer.state = state; - pageInitializer.prompt = prompt; - pageInitializer.loginHint = loginHint; - Response response = pageInitializer.processInput(); - if (response != null) return response; - ClientSessionModel clientSession = pageInitializer.clientSession; - String accessCode = new ClientSessionCode(realm, clientSession).getCode(); - - if (idpHint != null && !"".equals(idpHint)) { - IdentityProviderModel identityProviderModel = realm.getIdentityProviderById(idpHint); - - if (identityProviderModel == null) { - return Flows.forms(session, realm, null, uriInfo) - .setError("Could not find an identity provider with the identifier [" + idpHint + "].") - .createErrorPage(); - } - return redirectToIdentityProvider(idpHint, accessCode); - } - - response = authManager.checkNonFormAuthentication(session, clientSession, realm, uriInfo, request, clientConnection, headers, event); - if (response != null) return response; - - // SPNEGO/Kerberos authentication TODO: This should be somehow pluggable instead of hardcoded this way (Authentication interceptors?) - HttpAuthenticationManager httpAuthManager = new HttpAuthenticationManager(session, clientSession, realm, uriInfo, request, clientConnection, event); - HttpAuthenticationManager.HttpAuthOutput httpAuthOutput = httpAuthManager.spnegoAuthenticate(); - if (httpAuthOutput.getResponse() != null) return httpAuthOutput.getResponse(); - - if (prompt != null && prompt.equals("none")) { - OIDCLoginProtocol oauth = new OIDCLoginProtocol(session, realm, uriInfo); - return oauth.cancelLogin(clientSession); - } - - List identityProviders = realm.getIdentityProviders(); - for (IdentityProviderModel identityProvider : identityProviders) { - if (identityProvider.isAuthenticateByDefault()) { - return redirectToIdentityProvider(identityProvider.getId(), accessCode); - } - } - - List requiredCredentials = realm.getRequiredCredentials(); - if (requiredCredentials.isEmpty()) { - if (!identityProviders.isEmpty()) { - if (identityProviders.size() == 1) { - return redirectToIdentityProvider(identityProviders.get(0).getId(), accessCode); - } - - return Flows.forms(session, realm, null, uriInfo).setError("Realm [" + this.realm.getName() + "] supports multiple identity providers. Could not determine which identity provider should be used to authenticate with.").createErrorPage(); - } - - return Flows.forms(session, realm, null, uriInfo).setError("Realm [" + this.realm.getName() + "] does not support any credential type.").createErrorPage(); - } - - LoginFormsProvider forms = Flows.forms(session, realm, clientSession.getClient(), uriInfo) - .setClientSessionCode(accessCode); - - // Attach state from SPNEGO authentication - if (httpAuthOutput.getChallenge() != null) { - httpAuthOutput.getChallenge().sendChallenge(forms); - } - - String rememberMeUsername = AuthenticationManager.getRememberMeUsername(realm, headers); - - if (loginHint != null || rememberMeUsername != null) { - MultivaluedMap formData = new MultivaluedMapImpl(); - - if (loginHint != null) { - formData.add(AuthenticationManager.FORM_USERNAME, loginHint); - } else { - formData.add(AuthenticationManager.FORM_USERNAME, rememberMeUsername); - formData.add("rememberMe", "on"); - } - - forms.setFormData(formData); - } - - return forms.createLogin(); - } - - /** - * Registration page. Must be redirected to from login page. - * - * @param responseType - * @param redirect - * @param clientId - * @param scopeParam - * @param state - * @return - */ - @Path("registrations") - @GET - public Response registerPage(@QueryParam(OIDCLoginProtocol.RESPONSE_TYPE_PARAM) String responseType, - @QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirect, - @QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId, - @QueryParam(OIDCLoginProtocol.SCOPE_PARAM) String scopeParam, - @QueryParam(OIDCLoginProtocol.STATE_PARAM) String state) { - event.event(EventType.REGISTER); - if (!realm.isRegistrationAllowed()) { - event.error(Errors.REGISTRATION_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Registration not allowed"); - } - - FrontPageInitializer pageInitializer = new FrontPageInitializer(); - pageInitializer.responseType = responseType; - pageInitializer.redirect = redirect; - pageInitializer.clientId = clientId; - pageInitializer.scopeParam = scopeParam; - pageInitializer.state = state; - Response response = pageInitializer.processInput(); - if (response != null) return response; - ClientSessionModel clientSession = pageInitializer.clientSession; - - - authManager.expireIdentityCookie(realm, uriInfo, clientConnection); - - return Flows.forms(session, realm, clientSession.getClient(), uriInfo) - .setClientSessionCode(new ClientSessionCode(realm, clientSession).getCode()) - .createRegistration(); - } - - /** - * Logout user session. User must be logged in via a session cookie. - * - * @param redirectUri - * @return - */ @Path("logout") - @GET - @NoCache - public Response logout(final @QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri) { - event.event(EventType.LOGOUT); - if (redirectUri != null) { - event.detail(Details.REDIRECT_URI, redirectUri); - } - // authenticate identity cookie, but ignore an access token timeout as we're logging out anyways. - AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers, false); - if (authResult != null) { - logout(authResult.getSession()); - } - - if (redirectUri != null) { - String validatedRedirect = verifyRealmRedirectUri(uriInfo, redirectUri, realm); - if (validatedRedirect == null) { - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect uri."); - } - return Response.status(302).location(UriBuilder.fromUri(validatedRedirect).build()).build(); - } else { - return Response.ok().build(); - } - } - - /** - * Logout a session via a non-browser invocation. Similar signature to refresh token except there is no grant_type. - * You must pass in the refresh token and - * authenticate the client if it is not public. - * - * If the client is a confidential client - * you must include the client-id (application name or oauth client name) and secret in an Basic Auth Authorization header. - * - * If the client is a public client, then you must include a "client_id" form parameter with the app's or oauth client's name. - * - * returns 204 if successful, 400 if not with a json error response. - * - * @param authorizationHeader - * @param form - * @return - */ - @Path("logout") - @POST - @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - public Response logoutToken(final @HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader, - final MultivaluedMap form) { - if (!checkSsl()) { - throw new NotAcceptableException("HTTPS required"); - } - - event.event(EventType.LOGOUT); - - ClientModel client = authorizeClient(authorizationHeader, form, event); - String refreshToken = form.getFirst(OAuth2Constants.REFRESH_TOKEN); - if (refreshToken == null) { - Map error = new HashMap(); - error.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_REQUEST); - error.put(OAuth2Constants.ERROR_DESCRIPTION, "No refresh token"); - event.error(Errors.INVALID_TOKEN); - return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build(); - } - try { - RefreshToken token = tokenManager.verifyRefreshToken(realm, refreshToken); - UserSessionModel userSessionModel = session.sessions().getUserSession(realm, token.getSessionState()); - if (userSessionModel != null) { - logout(userSessionModel); - } - } catch (OAuthErrorException e) { - Map error = new HashMap(); - error.put(OAuth2Constants.ERROR, e.getError()); - if (e.getDescription() != null) error.put(OAuth2Constants.ERROR_DESCRIPTION, e.getDescription()); - event.error(Errors.INVALID_TOKEN); - return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build(); - } - return Cors.add(request, Response.noContent()).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build(); - } - - private void logout(UserSessionModel userSession) { - authManager.logout(session, realm, userSession, uriInfo, clientConnection); - event.user(userSession.getUser()).session(userSession).success(); + public Object logout() { + LogoutEndpoint endpoint = new LogoutEndpoint(tokenManager, authManager, realm, event); + ResteasyProviderFactory.getInstance().injectProperties(endpoint); + return endpoint; } @Path("oauth/oob") @@ -1004,146 +234,4 @@ public class OIDCLoginProtocolService { } } - public static boolean matchesRedirects(Set validRedirects, String redirect) { - for (String validRedirect : validRedirects) { - if (validRedirect.endsWith("*")) { - // strip off * - int length = validRedirect.length() - 1; - validRedirect = validRedirect.substring(0, length); - if (redirect.startsWith(validRedirect)) return true; - // strip off trailing '/' - if (length - 1 > 0 && validRedirect.charAt(length - 1) == '/') length--; - validRedirect = validRedirect.substring(0, length); - if (validRedirect.equals(redirect)) return true; - } else if (validRedirect.equals(redirect)) return true; - } - return false; - } - - public static Set getValidateRedirectUris(RealmModel realm) { - Set redirects = new HashSet(); - for (ApplicationModel client : realm.getApplications()) { - for (String redirect : client.getRedirectUris()) { - redirects.add(redirect); - } - } - for (OAuthClientModel client : realm.getOAuthClients()) { - for (String redirect : client.getRedirectUris()) { - redirects.add(redirect); - } - } - return redirects; - } - - public static String verifyRealmRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm) { - Set validRedirects = getValidateRedirectUris(realm); - return verifyRedirectUri(uriInfo, redirectUri, realm, validRedirects); - } - - public static String verifyRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm, ClientModel client) { - Set validRedirects = client.getRedirectUris(); - return verifyRedirectUri(uriInfo, redirectUri, realm, validRedirects); - } - - public static String verifyRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm, Set validRedirects) { - if (redirectUri == null) { - if (validRedirects.size() != 1) return null; - String validRedirect = validRedirects.iterator().next(); - int idx = validRedirect.indexOf("/*"); - if (idx > -1) { - validRedirect = validRedirect.substring(0, idx); - } - redirectUri = validRedirect; - } else if (validRedirects.isEmpty()) { - logger.debug("No Redirect URIs supplied"); - redirectUri = null; - } else { - String r = redirectUri.indexOf('?') != -1 ? redirectUri.substring(0, redirectUri.indexOf('?')) : redirectUri; - Set resolveValidRedirects = resolveValidRedirects(uriInfo, validRedirects); - - boolean valid = matchesRedirects(resolveValidRedirects, r); - - if (!valid && r.startsWith(Constants.INSTALLED_APP_URL) && r.indexOf(':', Constants.INSTALLED_APP_URL.length()) >= 0) { - int i = r.indexOf(':', Constants.INSTALLED_APP_URL.length()); - - StringBuilder sb = new StringBuilder(); - sb.append(r.substring(0, i)); - - i = r.indexOf('/', i); - if (i >= 0) { - sb.append(r.substring(i)); - } - - r = sb.toString(); - - valid = matchesRedirects(resolveValidRedirects, r); - } - if (valid && redirectUri.startsWith("/")) { - redirectUri = relativeToAbsoluteURI(uriInfo, redirectUri); - } - redirectUri = valid ? redirectUri : null; - } - - if (Constants.INSTALLED_APP_URN.equals(redirectUri)) { - return Urls.realmInstalledAppUrnCallback(uriInfo.getBaseUri(), realm.getName()).toString(); - } else { - return redirectUri; - } - } - - public static Set resolveValidRedirects(UriInfo uriInfo, Set validRedirects) { - // If the valid redirect URI is relative (no scheme, host, port) then use the request's scheme, host, and port - Set resolveValidRedirects = new HashSet(); - for (String validRedirect : validRedirects) { - resolveValidRedirects.add(validRedirect); // add even relative urls. - if (validRedirect.startsWith("/")) { - validRedirect = relativeToAbsoluteURI(uriInfo, validRedirect); - logger.debugv("replacing relative valid redirect with: {0}", validRedirect); - resolveValidRedirects.add(validRedirect); - } - } - return resolveValidRedirects; - } - - public static String relativeToAbsoluteURI(UriInfo uriInfo, String relative) { - URI baseUri = uriInfo.getBaseUri(); - String uri = baseUri.getScheme() + "://" + baseUri.getHost(); - if (baseUri.getPort() != -1) { - uri += ":" + baseUri.getPort(); - } - relative = uri + relative; - return relative; - } - - private boolean checkSsl() { - if (uriInfo.getBaseUri().getScheme().equals("https")) { - return true; - } else { - return !realm.getSslRequired().isRequired(clientConnection); - } - } - - private Response createError(String error, String errorDescription, Response.Status status) { - Map e = new HashMap(); - e.put(OAuth2Constants.ERROR, error); - if (errorDescription != null) { - e.put(OAuth2Constants.ERROR_DESCRIPTION, errorDescription); - } - return Response.status(status).entity(e).type("application/json").build(); - } - - private Response redirectToIdentityProvider(String providerId, String accessCode) { - logger.debug("Automatically redirect to identity provider: " + providerId); - return Response.temporaryRedirect( - Urls.identityProviderAuthnRequest(this.uriInfo.getBaseUri(), providerId, this.realm.getName(), accessCode)) - .build(); - } - - TokenManager getTokenManager() { - return this.tokenManager; - } - - RealmModel getRealm() { - return this.realm; - } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java new file mode 100644 index 0000000000..54e40096d8 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java @@ -0,0 +1,70 @@ +package org.keycloak.protocol.oidc; + +import org.keycloak.OAuth2Constants; +import org.keycloak.models.RealmModel; +import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; +import org.keycloak.services.resources.RealmsResource; +import org.keycloak.wellknown.WellKnownProvider; + +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * @author Stian Thorgersen + */ +public class OIDCWellKnownProvider implements WellKnownProvider { + + public static final List DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = list("RS256"); + + public static final List DEFAULT_GRANT_TYPES_SUPPORTED = list(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN); + + public static final List DEFAULT_RESPONSE_TYPES_SUPPORTED = list(OAuth2Constants.CODE); + + public static final List DEFAULT_SUBJECT_TYPES_SUPPORTED = list("public"); + + public static final List DEFAULT_RESPONSE_MODES_SUPPORTED = list("query"); + + @Override + public Object getConfig(RealmModel realm, UriInfo uriInfo) { + UriBuilder uriBuilder = RealmsResource.protocolUrl(uriInfo); + + OIDCConfigurationRepresentation config = new OIDCConfigurationRepresentation(); + config.setIssuer(realm.getName()); + config.setAuthorizationEndpoint(uriBuilder.clone().path(OIDCLoginProtocolService.class, "auth").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); + config.setTokenEndpoint(uriBuilder.clone().path(OIDCLoginProtocolService.class, "token").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); + config.setUserinfoEndpoint(uriBuilder.clone().path(OIDCLoginProtocolService.class, "issueUserInfo").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); + config.setJwksUri(uriBuilder.clone().path(OIDCLoginProtocolService.class, "certs").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); + + config.setIdTokenSigningAlgValuesSupported(DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED); + config.setResponseTypesSupported(DEFAULT_RESPONSE_TYPES_SUPPORTED); + config.setSubjectTypesSupported(DEFAULT_SUBJECT_TYPES_SUPPORTED); + config.setResponseModesSupported(DEFAULT_RESPONSE_MODES_SUPPORTED); + + if (!realm.isPasswordCredentialGrantAllowed()) { + config.setGrantTypesSupported(DEFAULT_GRANT_TYPES_SUPPORTED); + } else { + List grantTypes = new LinkedList<>(DEFAULT_GRANT_TYPES_SUPPORTED); + grantTypes.add(OAuth2Constants.PASSWORD); + config.setGrantTypesSupported(grantTypes); + } + + return config; + } + + @Override + public void close() { + } + + private static List list(String... values) { + List s = new LinkedList<>(); + for (String v : values) { + s.add(v); + } + return s; + } + +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProviderFactory.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProviderFactory.java new file mode 100644 index 0000000000..e49a993c55 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProviderFactory.java @@ -0,0 +1,40 @@ +package org.keycloak.protocol.oidc; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.wellknown.WellKnownProvider; +import org.keycloak.wellknown.WellKnownProviderFactory; + +/** + * @author Stian Thorgersen + */ +public class OIDCWellKnownProviderFactory implements WellKnownProviderFactory { + + private WellKnownProvider provider; + + @Override + public WellKnownProvider create(KeycloakSession session) { + return provider; + } + + @Override + public void init(Config.Scope config) { + provider = new OIDCWellKnownProvider(); + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + provider = null; + } + + @Override + public String getId() { + return "openid-configuration"; + } + +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java new file mode 100644 index 0000000000..6f6441f55e --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java @@ -0,0 +1,321 @@ +package org.keycloak.protocol.oidc.endpoints; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.specimpl.MultivaluedMapImpl; +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.ClientConnection; +import org.keycloak.OAuth2Constants; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; +import org.keycloak.login.LoginFormsProvider; +import org.keycloak.models.ApplicationModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RequiredCredentialModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.utils.RedirectUtils; +import org.keycloak.services.ErrorPageException; +import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.ClientSessionCode; +import org.keycloak.services.managers.HttpAuthenticationManager; +import org.keycloak.services.resources.flows.Flows; +import org.keycloak.services.resources.flows.Urls; + +import javax.ws.rs.GET; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import java.util.List; + +/** + * @author Stian Thorgersen + */ +public class AuthorizationEndpoint { + + private static final Logger logger = Logger.getLogger(AuthorizationEndpoint.class); + + private enum Action { + REGISTER, CODE + } + + @Context + private KeycloakSession session; + + @Context + private HttpRequest request; + + @Context + private HttpHeaders headers; + + @Context + private UriInfo uriInfo; + + @Context + private ClientConnection clientConnection; + + private final AuthenticationManager authManager; + private final RealmModel realm; + private final EventBuilder event; + + private ClientModel client; + private ClientSessionModel clientSession; + + private Action action; + + private String clientId; + private String redirectUri; + private String redirectUriParam; + private String responseType; + private String state; + private String scope; + private String loginHint; + private String prompt; + private String idpHint; + + private String legacyResponseType; + + public AuthorizationEndpoint(AuthenticationManager authManager, RealmModel realm, EventBuilder event) { + this.authManager = authManager; + this.realm = realm; + this.event = event; + event.event(EventType.LOGIN); + } + + @GET + public Response build() { + switch (action) { + case REGISTER: + return buildRegister(); + case CODE: + return buildAuthorizationCodeAuthorizationResponse(); + } + + throw new RuntimeException("Unknown action " + action); + } + + /** + * @deprecated + */ + public AuthorizationEndpoint legacy(String legacyResponseType) { + // TODO Change to warn once adapters has been updated + logger.debugv("Invoking deprecated endpoint {0}", uriInfo.getRequestUri()); + this.legacyResponseType = legacyResponseType; + return this; + } + + public AuthorizationEndpoint register() { + event.event(EventType.REGISTER); + action = Action.REGISTER; + + if (!realm.isRegistrationAllowed()) { + throw new ErrorPageException(session, realm, uriInfo, "Registration not allowed"); + } + + return this; + } + + public AuthorizationEndpoint init() { + MultivaluedMap params = uriInfo.getQueryParameters(); + + clientId = params.getFirst(OIDCLoginProtocol.CLIENT_ID_PARAM); + responseType = params.getFirst(OIDCLoginProtocol.RESPONSE_TYPE_PARAM); + redirectUriParam = params.getFirst(OIDCLoginProtocol.REDIRECT_URI_PARAM); + state = params.getFirst(OIDCLoginProtocol.STATE_PARAM); + scope = params.getFirst(OIDCLoginProtocol.SCOPE_PARAM); + loginHint = params.getFirst(OIDCLoginProtocol.LOGIN_HINT_PARAM); + prompt = params.getFirst(OIDCLoginProtocol.REDIRECT_URI_PARAM); + idpHint = params.getFirst(OIDCLoginProtocol.K_IDP_HINT); + + checkSsl(); + checkRealm(); + checkClient(); + checkResponseType(); + checkRedirectUri(); + + createClientSession(); + + return this; + } + + private void checkSsl() { + if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) { + event.error(Errors.SSL_REQUIRED); + throw new ErrorPageException(session, realm, uriInfo, "HTTPS required"); + } + } + + private void checkRealm() { + if (!realm.isEnabled()) { + event.error(Errors.REALM_DISABLED); + throw new ErrorPageException(session, realm, uriInfo, "Realm not enabled"); + } + } + + private void checkClient() { + if (clientId == null) { + event.error(Errors.INVALID_REQUEST); + throw new ErrorPageException(session, realm, uriInfo, "Missing paramater: " + OIDCLoginProtocol.CLIENT_ID_PARAM); + } + + event.client(clientId); + + client = realm.findClient(clientId); + if (client == null) { + event.error(Errors.CLIENT_NOT_FOUND); + throw new ErrorPageException(session, realm, uriInfo, "Client not found"); + } + + if ((client instanceof ApplicationModel) && ((ApplicationModel) client).isBearerOnly()) { + event.error(Errors.NOT_ALLOWED); + throw new ErrorPageException(session, realm, uriInfo, "Bearer only clients are not allowed to initiate browser login"); + } + + if (client.isDirectGrantsOnly()) { + event.error(Errors.NOT_ALLOWED); + throw new ErrorPageException(session, realm, uriInfo, "Direct grants only clients are not allowed to initiate browser login"); + } + } + + private void checkResponseType() { + if (responseType == null) { + if (legacyResponseType != null) { + responseType = legacyResponseType; + } else { + event.error(Errors.INVALID_REQUEST); + throw new ErrorPageException(session, realm, uriInfo, "Missing query parameter: " + OIDCLoginProtocol.RESPONSE_TYPE_PARAM); + } + } + + event.detail(Details.RESPONSE_TYPE, responseType); + + if (responseType.equals(OAuth2Constants.CODE)) { + action = Action.CODE; + } else { + event.error(Errors.INVALID_REQUEST); + throw new ErrorPageException(session, realm, uriInfo, "Invalid " + OIDCLoginProtocol.RESPONSE_TYPE_PARAM); + } + } + + private void checkRedirectUri() { + event.detail(Details.REDIRECT_URI, redirectUriParam); + + redirectUri = RedirectUtils.verifyRedirectUri(uriInfo, redirectUriParam, realm, client); + if (redirectUri == null) { + event.error(Errors.INVALID_REDIRECT_URI); + throw new ErrorPageException(session, realm, uriInfo, "Invalid " + OIDCLoginProtocol.REDIRECT_URI_PARAM); + } + } + + private void createClientSession() { + clientSession = session.sessions().createClientSession(realm, client); + clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL); + clientSession.setRedirectUri(redirectUri); + clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE); + clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret()); + clientSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, responseType); + clientSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUriParam); + + if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state); + if (scope != null) clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, scope); + if (loginHint != null) clientSession.setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, loginHint); + if (prompt != null) clientSession.setNote(OIDCLoginProtocol.PROMPT_PARAM, prompt); + if (idpHint != null) clientSession.setNote(OIDCLoginProtocol.K_IDP_HINT, idpHint); + } + + private Response buildAuthorizationCodeAuthorizationResponse() { + String accessCode = new ClientSessionCode(realm, clientSession).getCode(); + + if (idpHint != null && !"".equals(idpHint)) { + IdentityProviderModel identityProviderModel = realm.getIdentityProviderById(idpHint); + + if (identityProviderModel == null) { + return Flows.forms(session, realm, null, uriInfo) + .setError("Could not find an identity provider with the identifier [" + idpHint + "].") + .createErrorPage(); + } + return buildRedirectToIdentityProvider(idpHint, accessCode); + } + + Response response = authManager.checkNonFormAuthentication(session, clientSession, realm, uriInfo, request, clientConnection, headers, event); + if (response != null) return response; + + // SPNEGO/Kerberos authentication TODO: This should be somehow pluggable instead of hardcoded this way (Authentication interceptors?) + HttpAuthenticationManager httpAuthManager = new HttpAuthenticationManager(session, clientSession, realm, uriInfo, request, clientConnection, event); + HttpAuthenticationManager.HttpAuthOutput httpAuthOutput = httpAuthManager.spnegoAuthenticate(); + if (httpAuthOutput.getResponse() != null) return httpAuthOutput.getResponse(); + + if (prompt != null && prompt.equals("none")) { + OIDCLoginProtocol oauth = new OIDCLoginProtocol(session, realm, uriInfo); + return oauth.cancelLogin(clientSession); + } + + List identityProviders = realm.getIdentityProviders(); + for (IdentityProviderModel identityProvider : identityProviders) { + if (identityProvider.isAuthenticateByDefault()) { + return buildRedirectToIdentityProvider(identityProvider.getId(), accessCode); + } + } + + List requiredCredentials = realm.getRequiredCredentials(); + if (requiredCredentials.isEmpty()) { + if (!identityProviders.isEmpty()) { + if (identityProviders.size() == 1) { + return buildRedirectToIdentityProvider(identityProviders.get(0).getId(), accessCode); + } + + return Flows.forms(session, realm, null, uriInfo).setError("Realm [" + realm.getName() + "] supports multiple identity providers. Could not determine which identity provider should be used to authenticate with.").createErrorPage(); + } + + return Flows.forms(session, realm, null, uriInfo).setError("Realm [" + realm.getName() + "] does not support any credential type.").createErrorPage(); + } + + LoginFormsProvider forms = Flows.forms(session, realm, clientSession.getClient(), uriInfo) + .setClientSessionCode(accessCode); + + // Attach state from SPNEGO authentication + if (httpAuthOutput.getChallenge() != null) { + httpAuthOutput.getChallenge().sendChallenge(forms); + } + + String rememberMeUsername = AuthenticationManager.getRememberMeUsername(realm, headers); + + if (loginHint != null || rememberMeUsername != null) { + MultivaluedMap formData = new MultivaluedMapImpl(); + + if (loginHint != null) { + formData.add(AuthenticationManager.FORM_USERNAME, loginHint); + } else { + formData.add(AuthenticationManager.FORM_USERNAME, rememberMeUsername); + formData.add("rememberMe", "on"); + } + + forms.setFormData(formData); + } + + return forms.createLogin(); + } + + private Response buildRegister() { + authManager.expireIdentityCookie(realm, uriInfo, clientConnection); + + return Flows.forms(session, realm, client, uriInfo) + .setClientSessionCode(new ClientSessionCode(realm, clientSession).getCode()) + .createRegistration(); + } + + private Response buildRedirectToIdentityProvider(String providerId, String accessCode) { + logger.debug("Automatically redirect to identity provider: " + providerId); + return Response.temporaryRedirect( + Urls.identityProviderAuthnRequest(this.uriInfo.getBaseUri(), providerId, this.realm.getName(), accessCode)) + .build(); + } + +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java new file mode 100644 index 0000000000..30359d12f3 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java @@ -0,0 +1,91 @@ +package org.keycloak.protocol.oidc.endpoints; + +import org.jboss.resteasy.spi.BadRequestException; +import org.jboss.resteasy.spi.NotFoundException; +import org.keycloak.Config; +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; +import org.keycloak.protocol.oidc.utils.RedirectUtils; +import org.keycloak.util.StreamUtil; +import org.keycloak.util.UriUtils; + +import javax.ws.rs.GET; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.CacheControl; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import java.io.IOException; +import java.io.InputStream; + +/** + * @author Stian Thorgersen + */ +public class LoginStatusIframeEndpoint { + + @Context + private UriInfo uriInfo; + + private RealmModel realm; + + public LoginStatusIframeEndpoint(RealmModel realm) { + this.realm = realm; + } + + @GET + @Produces(MediaType.TEXT_HTML) + public Response getLoginStatusIframe(@QueryParam("client_id") String client_id, + @QueryParam("origin") String origin) { + if (!UriUtils.isOrigin(origin)) { + throw new BadRequestException("Invalid origin"); + } + + ClientModel client = realm.findClient(client_id); + if (client == null) { + throw new NotFoundException("could not find client"); + } + + InputStream is = getClass().getClassLoader().getResourceAsStream("login-status-iframe.html"); + if (is == null) throw new NotFoundException("Could not find login-status-iframe.html "); + + boolean valid = false; + for (String o : client.getWebOrigins()) { + if (o.equals("*") || o.equals(origin)) { + valid = true; + break; + } + } + + for (String r : RedirectUtils.resolveValidRedirects(uriInfo, client.getRedirectUris())) { + int i = r.indexOf('/', 8); + if (i != -1) { + r = r.substring(0, i); + } + + if (r.equals(origin)) { + valid = true; + break; + } + } + + if (!valid) { + throw new BadRequestException("Invalid origin"); + } + + try { + String file = StreamUtil.readString(is); + file = file.replace("ORIGIN", origin); + + CacheControl cacheControl = new CacheControl(); + cacheControl.setNoTransform(false); + cacheControl.setMaxAge(Config.scope("theme").getInt("staticMaxAge", -1)); + + return Response.ok(file).cacheControl(cacheControl).build(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java new file mode 100644 index 0000000000..16ab80c043 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java @@ -0,0 +1,166 @@ +package org.keycloak.protocol.oidc.endpoints; + +import org.jboss.resteasy.annotations.cache.NoCache; +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.ClientConnection; +import org.keycloak.OAuth2Constants; +import org.keycloak.OAuthErrorException; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; +import org.keycloak.models.ApplicationModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.TokenManager; +import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil; +import org.keycloak.protocol.oidc.utils.RedirectUtils; +import org.keycloak.representations.RefreshToken; +import org.keycloak.services.ErrorResponseException; +import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.resources.Cors; +import org.keycloak.services.resources.flows.Flows; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; + +/** + * @author Stian Thorgersen + */ +public class LogoutEndpoint { + + @Context + private KeycloakSession session; + + @Context + private ClientConnection clientConnection; + + @Context + private HttpRequest request; + + @Context + private HttpHeaders headers; + + @Context + private UriInfo uriInfo; + + private TokenManager tokenManager; + private AuthenticationManager authManager; + private RealmModel realm; + private EventBuilder event; + + public LogoutEndpoint(TokenManager tokenManager, AuthenticationManager authManager, RealmModel realm, EventBuilder event) { + this.tokenManager = tokenManager; + this.authManager = authManager; + this.realm = realm; + this.event = event; + } + + /** + * Logout user session. User must be logged in via a session cookie. + * + * @param redirectUri + * @return + */ + @GET + @NoCache + public Response logout(final @QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri) { + event.event(EventType.LOGOUT); + if (redirectUri != null) { + event.detail(Details.REDIRECT_URI, redirectUri); + } + // authenticate identity cookie, but ignore an access token timeout as we're logging out anyways. + AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers, false); + if (authResult != null) { + logout(authResult.getSession()); + } + + if (redirectUri != null) { + String validatedRedirect = RedirectUtils.verifyRealmRedirectUri(uriInfo, redirectUri, realm); + if (validatedRedirect == null) { + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect uri."); + } + return Response.status(302).location(UriBuilder.fromUri(validatedRedirect).build()).build(); + } else { + return Response.ok().build(); + } + } + + /** + * Logout a session via a non-browser invocation. Similar signature to refresh token except there is no grant_type. + * You must pass in the refresh token and + * authenticate the client if it is not public. + * + * If the client is a confidential client + * you must include the client-id (application name or oauth client name) and secret in an Basic Auth Authorization header. + * + * If the client is a public client, then you must include a "client_id" form parameter with the app's or oauth client's name. + * + * returns 204 if successful, 400 if not with a json error response. + * + * @param authorizationHeader + * @param form + * @return + */ + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response logoutToken(final @HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader, + final MultivaluedMap form) { + checkSsl(); + + event.event(EventType.LOGOUT); + + ClientModel client = authorizeClient(authorizationHeader, form, event); + String refreshToken = form.getFirst(OAuth2Constants.REFRESH_TOKEN); + if (refreshToken == null) { + event.error(Errors.INVALID_TOKEN); + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "No refresh token", Response.Status.BAD_REQUEST); + } + try { + RefreshToken token = tokenManager.verifyRefreshToken(realm, refreshToken); + UserSessionModel userSessionModel = session.sessions().getUserSession(realm, token.getSessionState()); + if (userSessionModel != null) { + logout(userSessionModel); + } + } catch (OAuthErrorException e) { + event.error(Errors.INVALID_TOKEN); + throw new ErrorResponseException(e.getError(), e.getDescription(), Response.Status.BAD_REQUEST); + } + return Cors.add(request, Response.noContent()).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build(); + } + + private void logout(UserSessionModel userSession) { + authManager.logout(session, realm, userSession, uriInfo, clientConnection); + event.user(userSession.getUser()).session(userSession).success(); + } + + private ClientModel authorizeClient(String authorizationHeader, MultivaluedMap formData, EventBuilder event) { + ClientModel client = AuthorizeClientUtil.authorizeClient(authorizationHeader, formData, event, realm); + + if ( (client instanceof ApplicationModel) && ((ApplicationModel)client).isBearerOnly()) { + throw new ErrorResponseException("invalid_client", "Bearer-only not allowed", Response.Status.BAD_REQUEST); + } + + return client; + } + + private void checkSsl() { + if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) { + throw new ErrorResponseException("invalid_request", "HTTPS required", Response.Status.FORBIDDEN); + } + } + +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java new file mode 100644 index 0000000000..ab78e6c7a9 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -0,0 +1,350 @@ +package org.keycloak.protocol.oidc.endpoints; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.ClientConnection; +import org.keycloak.OAuth2Constants; +import org.keycloak.OAuthErrorException; +import org.keycloak.constants.AdapterConstants; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; +import org.keycloak.models.ApplicationModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.UserSessionProvider; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.OIDCLoginProtocolService; +import org.keycloak.protocol.oidc.TokenManager; +import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.services.ErrorResponseException; +import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.ClientSessionCode; +import org.keycloak.services.resources.Cors; + +import javax.ws.rs.Consumes; +import javax.ws.rs.OPTIONS; +import javax.ws.rs.POST; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import java.util.Map; + +/** + * @author Stian Thorgersen + */ +public class TokenEndpoint { + + private static final Logger logger = Logger.getLogger(TokenEndpoint.class); + + private enum Action { + AUTHORIZATION_CODE, REFRESH_TOKEN, PASSWORD + } + + @Context + private KeycloakSession session; + + @Context + private HttpRequest request; + + @Context + private HttpHeaders headers; + + @Context + private UriInfo uriInfo; + + @Context + private ClientConnection clientConnection; + + private final TokenManager tokenManager; + private final AuthenticationManager authManager; + private final RealmModel realm; + private final EventBuilder event; + + private Action action; + + private String clientId; + private String grantType; + private String code; + private String redirectUri; + + private String legacyGrantType; + + public TokenEndpoint(TokenManager tokenManager, AuthenticationManager authManager, RealmModel realm, EventBuilder event) { + this.tokenManager = tokenManager; + this.authManager = authManager; + this.realm = realm; + this.event = event; + } + + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response build(final MultivaluedMap formData) { + switch (action) { + case AUTHORIZATION_CODE: + return buildAuthorizationCodeAccessTokenResponse(formData); + case REFRESH_TOKEN: + return buildRefreshToken(formData); + case PASSWORD: + return buildResourceOwnerPasswordCredentialsGrant(formData); + } + + throw new RuntimeException("Unknown action " + action); + } + + @OPTIONS + public Response preflight() { + if (logger.isDebugEnabled()) { + logger.debugv("CORS preflight from: {0}", headers.getRequestHeaders().getFirst("Origin")); + } + return Cors.add(request, Response.ok()).auth().preflight().build(); + } + + /** + * @deprecated + */ + public TokenEndpoint legacy(String legacyGrantType) { + // TODO Change to warn once adapters has been updated + logger.debugv("Invoking deprecated endpoint {0}", uriInfo.getRequestUri()); + this.legacyGrantType = legacyGrantType; + return this; + } + + public TokenEndpoint init() { + MultivaluedMap params = uriInfo.getQueryParameters(); + + clientId = params.getFirst(OIDCLoginProtocol.CLIENT_ID_PARAM); + grantType = params.getFirst(OIDCLoginProtocol.GRANT_TYPE_PARAM); + code = params.getFirst(OIDCLoginProtocol.CODE_PARAM); + redirectUri = params.getFirst(OIDCLoginProtocol.REDIRECT_URI_PARAM); + + checkSsl(); + checkRealm(); + checkGrantType(); + + return this; + } + + private void checkSsl() { + if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) { + throw new ErrorResponseException("invalid_request", "HTTPS required", Response.Status.FORBIDDEN); + } + } + + private void checkRealm() { + if (!realm.isEnabled()) { + throw new ErrorResponseException("access_denied", "Realm not enabled", Response.Status.FORBIDDEN); + } + } + + private ClientModel authorizeClient(final MultivaluedMap formData) { + String authorizationHeader = headers.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION); + ClientModel client = AuthorizeClientUtil.authorizeClient(authorizationHeader, formData, event, realm); + + if ((client instanceof ApplicationModel) && ((ApplicationModel) client).isBearerOnly()) { + throw new ErrorResponseException("invalid_client", "Bearer-only not allowed", Response.Status.BAD_REQUEST); + } + + return client; + } + + private void checkGrantType() { + if (grantType == null) { + if (legacyGrantType != null) { + grantType = legacyGrantType; + } else { + throw new ErrorResponseException("invalid_request", "Missing query parameter: " + OIDCLoginProtocol.GRANT_TYPE_PARAM, Response.Status.BAD_REQUEST); + } + } + + if (grantType.equals(OAuth2Constants.AUTHORIZATION_CODE)) { + event.event(EventType.CODE_TO_TOKEN); + action = Action.AUTHORIZATION_CODE; + } else if (grantType.equals(OAuth2Constants.REFRESH_TOKEN)) { + event.event(EventType.REFRESH_TOKEN); + action = Action.REFRESH_TOKEN; + } else if (grantType.equals(OAuth2Constants.PASSWORD)) { + event.event(EventType.LOGIN); + action = Action.PASSWORD; + } else { + throw new ErrorResponseException(Errors.INVALID_REQUEST, "Invalid " + OIDCLoginProtocol.GRANT_TYPE_PARAM, Response.Status.BAD_REQUEST); + } + } + + public Response buildAuthorizationCodeAccessTokenResponse(final MultivaluedMap formData) { + String code = formData.getFirst(OAuth2Constants.CODE); + if (code == null) { + event.error(Errors.INVALID_CODE); + throw new ErrorResponseException("invalid_request", "Missing parameter: " + OAuth2Constants.CODE, Response.Status.BAD_REQUEST); + } + + ClientSessionCode accessCode = ClientSessionCode.parse(code, session, realm); + if (accessCode == null) { + String[] parts = code.split("\\."); + if (parts.length == 2) { + try { + event.detail(Details.CODE_ID, new String(parts[1])); + } catch (Throwable t) { + } + } + event.error(Errors.INVALID_CODE); + throw new ErrorResponseException("invalid_grant", "Code not found", Response.Status.BAD_REQUEST); + } + + ClientSessionModel clientSession = accessCode.getClientSession(); + event.detail(Details.CODE_ID, clientSession.getId()); + if (!accessCode.isValid(ClientSessionModel.Action.CODE_TO_TOKEN)) { + event.error(Errors.INVALID_CODE); + throw new ErrorResponseException("invalid_grant", "Code is expired", Response.Status.BAD_REQUEST); + } + + accessCode.setAction(null); + UserSessionModel userSession = clientSession.getUserSession(); + event.user(userSession.getUser()); + event.session(userSession.getId()); + + ClientModel client = authorizeClient(formData); + + String redirectUri = clientSession.getNote(OIDCLoginProtocol.REDIRECT_URI_PARAM); + if (redirectUri != null && !redirectUri.equals(formData.getFirst(OAuth2Constants.REDIRECT_URI))) { + event.error(Errors.INVALID_CODE); + throw new ErrorResponseException("invalid_grant", "Incorrect redirect_uri", Response.Status.BAD_REQUEST); + } + + if (!client.getClientId().equals(clientSession.getClient().getClientId())) { + event.error(Errors.INVALID_CODE); + throw new ErrorResponseException("invalid_grant", "Auth error", Response.Status.BAD_REQUEST); + } + + UserModel user = session.users().getUserById(userSession.getUser().getId(), realm); + if (user == null) { + event.error(Errors.USER_NOT_FOUND); + throw new ErrorResponseException("invalid_grant", "User not found", Response.Status.BAD_REQUEST); + } + + if (!user.isEnabled()) { + event.error(Errors.USER_DISABLED); + throw new ErrorResponseException("invalid_grant", "User disabled", Response.Status.BAD_REQUEST); + } + + if (!AuthenticationManager.isSessionValid(realm, userSession)) { + event.error(Errors.USER_SESSION_NOT_FOUND); + throw new ErrorResponseException("invalid_grant", "Session not active", Response.Status.BAD_REQUEST); + } + + String adapterSessionId = formData.getFirst(AdapterConstants.APPLICATION_SESSION_STATE); + if (adapterSessionId != null) { + String adapterSessionHost = formData.getFirst(AdapterConstants.APPLICATION_SESSION_HOST); + logger.debugf("Adapter Session '%s' saved in ClientSession for client '%s'. Host is '%s'", adapterSessionId, client.getClientId(), adapterSessionHost); + + event.detail(AdapterConstants.APPLICATION_SESSION_STATE, adapterSessionId); + clientSession.setNote(AdapterConstants.APPLICATION_SESSION_STATE, adapterSessionId); + event.detail(AdapterConstants.APPLICATION_SESSION_HOST, adapterSessionHost); + clientSession.setNote(AdapterConstants.APPLICATION_SESSION_HOST, adapterSessionHost); + } + + AccessToken token = tokenManager.createClientAccessToken(session, accessCode.getRequestedRoles(), realm, client, user, userSession, clientSession); + + AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession) + .accessToken(token) + .generateIDToken() + .generateRefreshToken().build(); + + event.success(); + + return Cors.add(request, Response.ok(res).type(MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build(); + } + + public Response buildRefreshToken(final MultivaluedMap formData) { + ClientModel client = authorizeClient(formData); + + String refreshToken = formData.getFirst(OAuth2Constants.REFRESH_TOKEN); + if (refreshToken == null) { + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "No refresh token", Response.Status.BAD_REQUEST); + } + + AccessTokenResponse res; + try { + res = tokenManager.refreshAccessToken(session, uriInfo, clientConnection, realm, client, refreshToken, event); + } catch (OAuthErrorException e) { + event.error(Errors.INVALID_TOKEN); + throw new ErrorResponseException(e.getError(), e.getDescription(), Response.Status.BAD_REQUEST); + } + + event.success(); + + return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build(); + } + + public Response buildResourceOwnerPasswordCredentialsGrant(final MultivaluedMap formData) { + if (!realm.isPasswordCredentialGrantAllowed()) { + throw new ErrorResponseException("not_enabled", "Direct Grant REST API not enabled", Response.Status.FORBIDDEN); + } + + event.detail(Details.AUTH_METHOD, "oauth_credentials").detail(Details.RESPONSE_TYPE, "token"); + + String username = formData.getFirst(AuthenticationManager.FORM_USERNAME); + if (username == null) { + event.error(Errors.USERNAME_MISSING); + throw new ErrorResponseException("invalid_request", "Missing parameter: username", Response.Status.UNAUTHORIZED); + } + event.detail(Details.USERNAME, username); + + UserModel user = KeycloakModelUtils.findUserByNameOrEmail(session, realm, username); + if (user != null) event.user(user); + + ClientModel client = authorizeClient(formData); + + AuthenticationManager.AuthenticationStatus authenticationStatus = authManager.authenticateForm(session, clientConnection, realm, formData); + Map err; + + switch (authenticationStatus) { + case SUCCESS: + break; + case ACCOUNT_TEMPORARILY_DISABLED: + case ACTIONS_REQUIRED: + event.error(Errors.USER_TEMPORARILY_DISABLED); + throw new ErrorResponseException("invalid_grant", "Account temporarily disabled", Response.Status.BAD_REQUEST); + case ACCOUNT_DISABLED: + event.error(Errors.USER_DISABLED); + throw new ErrorResponseException("invalid_grant", "Account disabled", Response.Status.BAD_REQUEST); + default: + event.error(Errors.INVALID_USER_CREDENTIALS); + throw new ErrorResponseException("invalid_grant", "Invalid user credentials", Response.Status.UNAUTHORIZED); + } + + String scope = formData.getFirst(OAuth2Constants.SCOPE); + + UserSessionProvider sessions = session.sessions(); + + UserSessionModel userSession = sessions.createUserSession(realm, user, username, clientConnection.getRemoteAddr(), "oauth_credentials", false); + event.session(userSession); + + ClientSessionModel clientSession = sessions.createClientSession(realm, client); + clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL); + + TokenManager.attachClientSession(userSession, clientSession); + + AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession) + .generateAccessToken(session, scope, client, user, userSession, clientSession) + .generateRefreshToken() + .generateIDToken() + .build(); + + event.success(); + + return Response.ok(res, MediaType.APPLICATION_JSON_TYPE).build(); + } + +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/protocol/oidc/UserInfoService.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java similarity index 55% rename from services/src/main/java/org/keycloak/protocol/oidc/UserInfoService.java rename to services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java index 9958ae0b3a..1e6fc262cb 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/UserInfoService.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java @@ -15,13 +15,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.protocol.oidc; +package org.keycloak.protocol.oidc.endpoints; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpResponse; import org.jboss.resteasy.spi.UnauthorizedException; import org.keycloak.ClientConnection; +import org.keycloak.OAuthErrorException; +import org.keycloak.RSATokenVerifier; import org.keycloak.events.Details; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; @@ -30,8 +32,11 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.representations.AccessToken; +import org.keycloak.services.ErrorResponseException; import org.keycloak.services.managers.AppAuthManager; +import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.EventsManager; import org.keycloak.services.resources.Cors; @@ -53,7 +58,7 @@ import java.util.Map; /** * @author pedroigor */ -public class UserInfoService { +public class UserInfoEndpoint { @Context private HttpRequest request; @@ -69,23 +74,11 @@ public class UserInfoService { private final TokenManager tokenManager; private final AppAuthManager appAuthManager; - private final OIDCLoginProtocolService openIdConnectService; - private final RealmModel realmModel; + private final RealmModel realm; - public UserInfoService(OIDCLoginProtocolService openIDConnectService) { - this.realmModel = openIDConnectService.getRealm(); - - if (this.realmModel == null) { - throw new RuntimeException("Null realm."); - } - - this.tokenManager = openIDConnectService.getTokenManager(); - - if (this.tokenManager == null) { - throw new RuntimeException("Null token manager."); - } - - this.openIdConnectService = openIDConnectService; + public UserInfoEndpoint(TokenManager tokenManager, RealmModel realm) { + this.realm = realm; + this.tokenManager = tokenManager; this.appAuthManager = new AppAuthManager(); } @@ -114,40 +107,35 @@ public class UserInfoService { return issueUserInfo(accessToken); } - private Response issueUserInfo(String token) { + private Response issueUserInfo(String tokenString) { + EventBuilder event = new EventsManager(realm, session, clientConnection).createEventBuilder() + .event(EventType.USER_INFO_REQUEST) + .detail(Details.AUTH_METHOD, Details.VALIDATE_ACCESS_TOKEN); + + AccessToken token = null; try { - EventBuilder event = new EventsManager(this.realmModel, this.session, this.clientConnection).createEventBuilder() - .event(EventType.USER_INFO_REQUEST) - .detail(Details.AUTH_METHOD, Details.VALIDATE_ACCESS_TOKEN); - - Response validationResponse = this.openIdConnectService.validateAccessToken(token); - - if (!AccessToken.class.isInstance(validationResponse.getEntity())) { - event.error(EventType.USER_INFO_REQUEST.name()); - return Response.fromResponse(validationResponse).status(Status.FORBIDDEN).build(); - } - - AccessToken accessToken = (AccessToken) validationResponse.getEntity(); - UserSessionModel userSession = session.sessions().getUserSession(realmModel, accessToken.getSessionState()); - ClientModel clientModel = realmModel.findClient(accessToken.getIssuedFor()); - UserModel userModel = userSession.getUser(); - AccessToken userInfo = new AccessToken(); - this.tokenManager.transformAccessToken(session, userInfo, realmModel, clientModel, userModel, userSession, null); - - event - .detail(Details.USERNAME, userModel.getUsername()) - .client(clientModel) - .session(userSession) - .user(userModel) - .success(); - - Map claims = new HashMap(); - claims.putAll(userInfo.getOtherClaims()); - claims.put("sub", userModel.getId()); - return Cors.add(request, Response.ok(claims)).auth().allowedOrigins(accessToken).build(); + token = RSATokenVerifier.verifyToken(tokenString, realm.getPublicKey(), realm.getName()); } catch (Exception e) { - throw new UnauthorizedException("Could not retrieve user info.", e); + throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Token invalid", Status.FORBIDDEN); } + + UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState()); + ClientModel clientModel = realm.findClient(token.getIssuedFor()); + UserModel userModel = userSession.getUser(); + AccessToken userInfo = new AccessToken(); + tokenManager.transformAccessToken(session, userInfo, realm, clientModel, userModel, userSession, null); + + event + .detail(Details.USERNAME, userModel.getUsername()) + .client(clientModel) + .session(userSession) + .user(userModel) + .success(); + + Map claims = new HashMap(); + claims.putAll(userInfo.getOtherClaims()); + claims.put("sub", userModel.getId()); + return Cors.add(request, Response.ok(claims)).auth().allowedOrigins(token).build(); } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/ValidateTokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/ValidateTokenEndpoint.java new file mode 100644 index 0000000000..caef4361e0 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/ValidateTokenEndpoint.java @@ -0,0 +1,103 @@ +package org.keycloak.protocol.oidc.endpoints; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.annotations.cache.NoCache; +import org.keycloak.ClientConnection; +import org.keycloak.OAuth2Constants; +import org.keycloak.OAuthErrorException; +import org.keycloak.RSATokenVerifier; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.protocol.oidc.TokenManager; +import org.keycloak.representations.AccessToken; +import org.keycloak.services.ErrorResponseException; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Stian Thorgersen + */ +public class ValidateTokenEndpoint { + + private static final Logger logger = Logger.getLogger(ValidateTokenEndpoint.class); + + @Context + private KeycloakSession session; + + @Context + private ClientConnection clientConnection; + + @Context + private UriInfo uriInfo; + + private TokenManager tokenManager; + private RealmModel realm; + private EventBuilder event; + + public ValidateTokenEndpoint(TokenManager tokenManager, RealmModel realm, EventBuilder event) { + this.tokenManager = tokenManager; + this.realm = realm; + this.event = event; + } + + /** + * Validate encoded access token. + * + * @param tokenString + * @return Unmarshalled token + */ + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public Response validateAccessToken(@QueryParam("access_token") String tokenString) { + checkSsl(); + + event.event(EventType.VALIDATE_ACCESS_TOKEN); + AccessToken token = null; + try { + token = RSATokenVerifier.verifyToken(tokenString, realm.getPublicKey(), realm.getName()); + } catch (Exception e) { + Map err = new HashMap(); + err.put(OAuth2Constants.ERROR, OAuthErrorException.INVALID_GRANT); + err.put(OAuth2Constants.ERROR_DESCRIPTION, "Token invalid"); + logger.error("Invalid token. Token verification failed."); + event.error(Errors.INVALID_TOKEN); + return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err) + .build(); + } + event.user(token.getSubject()).session(token.getSessionState()).detail(Details.VALIDATE_ACCESS_TOKEN, token.getId()); + + try { + tokenManager.validateToken(session, uriInfo, clientConnection, realm, token); + } catch (OAuthErrorException e) { + Map error = new HashMap(); + error.put(OAuth2Constants.ERROR, e.getError()); + if (e.getDescription() != null) error.put(OAuth2Constants.ERROR_DESCRIPTION, e.getDescription()); + event.error(Errors.INVALID_TOKEN); + return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build(); + } + event.success(); + + return Response.ok(token, MediaType.APPLICATION_JSON_TYPE).build(); + } + + private void checkSsl() { + if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) { + throw new ErrorResponseException("invalid_request", "HTTPS required", Response.Status.FORBIDDEN); + } + } + +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/representations/JSONWebKeySet.java b/services/src/main/java/org/keycloak/protocol/oidc/representations/JSONWebKeySet.java new file mode 100644 index 0000000000..3ac954729b --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/representations/JSONWebKeySet.java @@ -0,0 +1,22 @@ +package org.keycloak.protocol.oidc.representations; + +import org.codehaus.jackson.annotate.JsonProperty; +import org.keycloak.jose.jwk.JWK; + +/** + * @author Stian Thorgersen + */ +public class JSONWebKeySet { + + @JsonProperty("keys") + private JWK[] keys; + + public JWK[] getKeys() { + return keys; + } + + public void setKeys(JWK[] keys) { + this.keys = keys; + } + +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java new file mode 100644 index 0000000000..0760b64c9e --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java @@ -0,0 +1,123 @@ +package org.keycloak.protocol.oidc.representations; + +import org.codehaus.jackson.annotate.JsonProperty; + +import java.util.List; +import java.util.Set; + +/** + * @author Stian Thorgersen + */ + +public class OIDCConfigurationRepresentation { + + @JsonProperty("issuer") + private String issuer; + + @JsonProperty("authorization_endpoint") + private String authorizationEndpoint; + + @JsonProperty("token_endpoint") + private String tokenEndpoint; + + @JsonProperty("userinfo_endpoint") + private String userinfoEndpoint; + + @JsonProperty("jwks_uri") + private String jwksUri; + + @JsonProperty("grant_types_supported") + private List grantTypesSupported; + + @JsonProperty("response_types_supported") + private List responseTypesSupported; + + @JsonProperty("subject_types_supported") + private List subjectTypesSupported; + + @JsonProperty("id_token_signing_alg_values_supported") + private List idTokenSigningAlgValuesSupported; + + @JsonProperty("response_modes_supported") + private List responseModesSupported; + + public String getIssuer() { + return issuer; + } + + public void setIssuer(String issuer) { + this.issuer = issuer; + } + + public String getAuthorizationEndpoint() { + return authorizationEndpoint; + } + + public void setAuthorizationEndpoint(String authorizationEndpoint) { + this.authorizationEndpoint = authorizationEndpoint; + } + + public String getTokenEndpoint() { + return tokenEndpoint; + } + + public void setTokenEndpoint(String tokenEndpoint) { + this.tokenEndpoint = tokenEndpoint; + } + + public String getUserinfoEndpoint() { + return userinfoEndpoint; + } + + public void setUserinfoEndpoint(String userinfoEndpoint) { + this.userinfoEndpoint = userinfoEndpoint; + } + + public String getJwksUri() { + return jwksUri; + } + + public void setJwksUri(String jwksUri) { + this.jwksUri = jwksUri; + } + + public List getGrantTypesSupported() { + return grantTypesSupported; + } + + public void setGrantTypesSupported(List grantTypesSupported) { + this.grantTypesSupported = grantTypesSupported; + } + + public List getResponseTypesSupported() { + return responseTypesSupported; + } + + public void setResponseTypesSupported(List responseTypesSupported) { + this.responseTypesSupported = responseTypesSupported; + } + + public List getSubjectTypesSupported() { + return subjectTypesSupported; + } + + public void setSubjectTypesSupported(List subjectTypesSupported) { + this.subjectTypesSupported = subjectTypesSupported; + } + + public List getIdTokenSigningAlgValuesSupported() { + return idTokenSigningAlgValuesSupported; + } + + public void setIdTokenSigningAlgValuesSupported(List idTokenSigningAlgValuesSupported) { + this.idTokenSigningAlgValuesSupported = idTokenSigningAlgValuesSupported; + } + + public List getResponseModesSupported() { + return responseModesSupported; + } + + public void setResponseModesSupported(List responseModesSupported) { + this.responseModesSupported = responseModesSupported; + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java new file mode 100644 index 0000000000..1a78b641f9 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java @@ -0,0 +1,76 @@ +package org.keycloak.protocol.oidc.utils; + +import org.jboss.resteasy.spi.BadRequestException; +import org.jboss.resteasy.spi.UnauthorizedException; +import org.keycloak.OAuth2Constants; +import org.keycloak.events.Errors; +import org.keycloak.events.EventBuilder; +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; +import org.keycloak.util.BasicAuthHelper; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Stian Thorgersen + */ +public class AuthorizeClientUtil { + + public static ClientModel authorizeClient(String authorizationHeader, MultivaluedMap formData, EventBuilder event, RealmModel realm) { + String client_id; + String clientSecret; + if (authorizationHeader != null) { + String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader); + if (usernameSecret == null) { + throw new UnauthorizedException("Bad Authorization header", Response.status(401).header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"" + realm.getName() + "\"").build()); + } + client_id = usernameSecret[0]; + clientSecret = usernameSecret[1]; + } else { + client_id = formData.getFirst(OAuth2Constants.CLIENT_ID); + clientSecret = formData.getFirst("client_secret"); + } + + if (client_id == null) { + Map error = new HashMap(); + error.put(OAuth2Constants.ERROR, "invalid_client"); + error.put(OAuth2Constants.ERROR_DESCRIPTION, "Could not find client"); + throw new BadRequestException("Could not find client", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); + } + + event.client(client_id); + + ClientModel client = realm.findClient(client_id); + if (client == null) { + Map error = new HashMap(); + error.put(OAuth2Constants.ERROR, "invalid_client"); + error.put(OAuth2Constants.ERROR_DESCRIPTION, "Could not find client"); + event.error(Errors.CLIENT_NOT_FOUND); + throw new BadRequestException("Could not find client", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); + } + + if (!client.isEnabled()) { + Map error = new HashMap(); + error.put(OAuth2Constants.ERROR, "invalid_client"); + error.put(OAuth2Constants.ERROR_DESCRIPTION, "Client is not enabled"); + event.error(Errors.CLIENT_DISABLED); + throw new BadRequestException("Client is not enabled", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); + } + + if (!client.isPublicClient()) { + if (clientSecret == null || !client.validateSecret(clientSecret)) { + Map error = new HashMap(); + error.put(OAuth2Constants.ERROR, "unauthorized_client"); + event.error(Errors.INVALID_CLIENT_CREDENTIALS); + throw new BadRequestException("Unauthorized Client", Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); + } + } + + return client; + } + +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java new file mode 100644 index 0000000000..2fa3aeedb7 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java @@ -0,0 +1,134 @@ +package org.keycloak.protocol.oidc.utils; + +import org.jboss.logging.Logger; +import org.keycloak.models.ApplicationModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; +import org.keycloak.models.OAuthClientModel; +import org.keycloak.models.RealmModel; +import org.keycloak.services.resources.flows.Urls; + +import javax.ws.rs.core.UriInfo; +import java.net.URI; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Stian Thorgersen + */ +public class RedirectUtils { + + private static final Logger logger = Logger.getLogger(RedirectUtils.class); + + public static String verifyRealmRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm) { + Set validRedirects = getValidateRedirectUris(realm); + return verifyRedirectUri(uriInfo, redirectUri, realm, validRedirects); + } + + public static String verifyRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm, ClientModel client) { + Set validRedirects = client.getRedirectUris(); + return verifyRedirectUri(uriInfo, redirectUri, realm, validRedirects); + } + + public static Set resolveValidRedirects(UriInfo uriInfo, Set validRedirects) { + // If the valid redirect URI is relative (no scheme, host, port) then use the request's scheme, host, and port + Set resolveValidRedirects = new HashSet(); + for (String validRedirect : validRedirects) { + resolveValidRedirects.add(validRedirect); // add even relative urls. + if (validRedirect.startsWith("/")) { + validRedirect = relativeToAbsoluteURI(uriInfo, validRedirect); + logger.debugv("replacing relative valid redirect with: {0}", validRedirect); + resolveValidRedirects.add(validRedirect); + } + } + return resolveValidRedirects; + } + + private static Set getValidateRedirectUris(RealmModel realm) { + Set redirects = new HashSet(); + for (ApplicationModel client : realm.getApplications()) { + for (String redirect : client.getRedirectUris()) { + redirects.add(redirect); + } + } + for (OAuthClientModel client : realm.getOAuthClients()) { + for (String redirect : client.getRedirectUris()) { + redirects.add(redirect); + } + } + return redirects; + } + + private static String verifyRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm, Set validRedirects) { + if (redirectUri == null) { + if (validRedirects.size() != 1) return null; + String validRedirect = validRedirects.iterator().next(); + int idx = validRedirect.indexOf("/*"); + if (idx > -1) { + validRedirect = validRedirect.substring(0, idx); + } + redirectUri = validRedirect; + } else if (validRedirects.isEmpty()) { + logger.debug("No Redirect URIs supplied"); + redirectUri = null; + } else { + String r = redirectUri.indexOf('?') != -1 ? redirectUri.substring(0, redirectUri.indexOf('?')) : redirectUri; + Set resolveValidRedirects = resolveValidRedirects(uriInfo, validRedirects); + + boolean valid = matchesRedirects(resolveValidRedirects, r); + + if (!valid && r.startsWith(Constants.INSTALLED_APP_URL) && r.indexOf(':', Constants.INSTALLED_APP_URL.length()) >= 0) { + int i = r.indexOf(':', Constants.INSTALLED_APP_URL.length()); + + StringBuilder sb = new StringBuilder(); + sb.append(r.substring(0, i)); + + i = r.indexOf('/', i); + if (i >= 0) { + sb.append(r.substring(i)); + } + + r = sb.toString(); + + valid = matchesRedirects(resolveValidRedirects, r); + } + if (valid && redirectUri.startsWith("/")) { + redirectUri = relativeToAbsoluteURI(uriInfo, redirectUri); + } + redirectUri = valid ? redirectUri : null; + } + + if (Constants.INSTALLED_APP_URN.equals(redirectUri)) { + return Urls.realmInstalledAppUrnCallback(uriInfo.getBaseUri(), realm.getName()).toString(); + } else { + return redirectUri; + } + } + + private static String relativeToAbsoluteURI(UriInfo uriInfo, String relative) { + URI baseUri = uriInfo.getBaseUri(); + String uri = baseUri.getScheme() + "://" + baseUri.getHost(); + if (baseUri.getPort() != -1) { + uri += ":" + baseUri.getPort(); + } + relative = uri + relative; + return relative; + } + + private static boolean matchesRedirects(Set validRedirects, String redirect) { + for (String validRedirect : validRedirects) { + if (validRedirect.endsWith("*")) { + // strip off * + int length = validRedirect.length() - 1; + validRedirect = validRedirect.substring(0, length); + if (redirect.startsWith(validRedirect)) return true; + // strip off trailing '/' + if (length - 1 > 0 && validRedirect.charAt(length - 1) == '/') length--; + validRedirect = validRedirect.substring(0, length); + if (validRedirect.equals(redirect)) return true; + } else if (validRedirect.equals(redirect)) return true; + } + return false; + } + +} diff --git a/services/src/main/java/org/keycloak/services/ErrorPageException.java b/services/src/main/java/org/keycloak/services/ErrorPageException.java new file mode 100644 index 0000000000..3f8d435ff0 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/ErrorPageException.java @@ -0,0 +1,33 @@ +package org.keycloak.services; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.services.resources.flows.Flows; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +/** + * @author Stian Thorgersen + */ +public class ErrorPageException extends WebApplicationException { + + private final KeycloakSession session; + private final RealmModel realm; + private final UriInfo uriInfo; + private final String errorMessage; + + public ErrorPageException(KeycloakSession session, RealmModel realm, UriInfo uriInfo, String errorMessage) { + this.session = session; + this.realm = realm; + this.uriInfo = uriInfo; + this.errorMessage = errorMessage; + } + + @Override + public Response getResponse() { + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, errorMessage); + } + +} diff --git a/services/src/main/java/org/keycloak/services/ErrorResponseException.java b/services/src/main/java/org/keycloak/services/ErrorResponseException.java new file mode 100644 index 0000000000..bf9f278429 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/ErrorResponseException.java @@ -0,0 +1,39 @@ +package org.keycloak.services; + +import org.keycloak.OAuth2Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.services.resources.flows.Flows; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Stian Thorgersen + */ +public class ErrorResponseException extends WebApplicationException { + + private final String error; + private final String errorDescription; + private final Response.Status status; + + public ErrorResponseException(String error, String errorDescription, Response.Status status) { + this.error = error; + this.errorDescription = errorDescription; + this.status = status; + } + + @Override + public Response getResponse() { + Map e = new HashMap(); + e.put(OAuth2Constants.ERROR, error); + if (errorDescription != null) { + e.put(OAuth2Constants.ERROR_DESCRIPTION, errorDescription); + } + return Response.status(status).entity(e).type("application/json").build(); + } + +} diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index d286a14b70..8b5297b298 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -50,6 +50,7 @@ import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; +import org.keycloak.protocol.oidc.utils.RedirectUtils; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.ForbiddenException; @@ -824,7 +825,7 @@ public class AccountService { ApplicationModel application = realm.getApplicationByName(referrer); if (application != null) { if (referrerUri != null) { - referrerUri = OIDCLoginProtocolService.verifyRedirectUri(uriInfo, referrerUri, realm, application); + referrerUri = RedirectUtils.verifyRedirectUri(uriInfo, referrerUri, realm, application); } else { referrerUri = ResolveRelative.resolveRelativeUri(uriInfo.getRequestUri(), application.getBaseUrl()); } @@ -835,7 +836,7 @@ public class AccountService { } else if (referrerUri != null) { ClientModel client = realm.getOAuthClient(referrer); if (client != null) { - referrerUri = OIDCLoginProtocolService.verifyRedirectUri(uriInfo, referrerUri, realm, application); + referrerUri = RedirectUtils.verifyRedirectUri(uriInfo, referrerUri, realm, application); if (referrerUri != null) { return new String[]{referrer, referrerUri}; diff --git a/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java b/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java index b49058f6c0..1761138677 100755 --- a/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java +++ b/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java @@ -31,6 +31,7 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; +import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil; import org.keycloak.services.ForbiddenException; import org.keycloak.util.Time; @@ -154,7 +155,7 @@ public class ClientsManagementService { } protected ApplicationModel authorizeApplication(String authorizationHeader, MultivaluedMap formData) { - ClientModel client = OIDCLoginProtocolService.authorizeClientBase(authorizationHeader, formData, event, realm); + ClientModel client = AuthorizeClientUtil.authorizeClient(authorizationHeader, formData, event, realm); if (client.isPublicClient()) { Map error = new HashMap(); diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java index 8a50c69624..247a430f94 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -17,6 +17,7 @@ import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.BruteForceProtector; import org.keycloak.services.managers.EventsManager; import org.keycloak.services.managers.RealmManager; +import org.keycloak.wellknown.WellKnownProvider; import javax.ws.rs.GET; import javax.ws.rs.Path; @@ -79,10 +80,8 @@ public class RealmsResource { } @Path("{realm}/login-status-iframe.html") - @GET - @Produces(MediaType.TEXT_HTML) @Deprecated - public Response getLoginStatusIframe(final @PathParam("realm") String name, + public Object getLoginStatusIframe(final @PathParam("realm") String name, @QueryParam("client_id") String client_id, @QueryParam("origin") String origin) { // backward compatibility @@ -95,7 +94,7 @@ public class RealmsResource { OIDCLoginProtocolService endpoint = (OIDCLoginProtocolService)factory.createProtocolEndpoint(realm, event, authManager); ResteasyProviderFactory.getInstance().injectProperties(endpoint); - return endpoint.getLoginStatusIframe(client_id, origin); + return endpoint.getLoginStatusIframe(); } @@ -196,5 +195,15 @@ public class RealmsResource { return brokerService; } + @GET + @Path("{realm}/.well-known/{provider}") + @Produces(MediaType.APPLICATION_JSON) + public Response getWellKnown(final @PathParam("realm") String realmName, + final @PathParam("provider") String providerName) { + RealmManager realmManager = new RealmManager(session); + RealmModel realm = locateRealm(realmName, realmManager); + WellKnownProvider wellKnown = session.getProvider(WellKnownProvider.class, providerName); + return Response.ok(wellKnown.getConfig(realm, uriInfo)).build(); + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index e508a70ba8..5732688886 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -26,6 +26,7 @@ import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.TokenManager; +import org.keycloak.protocol.oidc.utils.RedirectUtils; import org.keycloak.representations.idm.ApplicationMappingsRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.FederatedIdentityRepresentation; @@ -721,7 +722,7 @@ public class UsersResource { String redirect; if(redirectUri != null){ - redirect = OIDCLoginProtocolService.verifyRedirectUri(uriInfo, redirectUri, realm, client); + redirect = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri, realm, client); if(redirect == null){ return Flows.errors().error("Invalid redirect uri.", Response.Status.BAD_REQUEST); } diff --git a/services/src/main/java/org/keycloak/wellknown/WellKnownProvider.java b/services/src/main/java/org/keycloak/wellknown/WellKnownProvider.java new file mode 100755 index 0000000000..d4b80d7795 --- /dev/null +++ b/services/src/main/java/org/keycloak/wellknown/WellKnownProvider.java @@ -0,0 +1,16 @@ +package org.keycloak.wellknown; + +import org.keycloak.models.RealmModel; +import org.keycloak.provider.Provider; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +/** + * @author Stian Thorgersen + */ +public interface WellKnownProvider extends Provider { + + Object getConfig(RealmModel realm, UriInfo uriInfo); + +} diff --git a/services/src/main/java/org/keycloak/wellknown/WellKnownProviderFactory.java b/services/src/main/java/org/keycloak/wellknown/WellKnownProviderFactory.java new file mode 100755 index 0000000000..21d8b819e9 --- /dev/null +++ b/services/src/main/java/org/keycloak/wellknown/WellKnownProviderFactory.java @@ -0,0 +1,10 @@ +package org.keycloak.wellknown; + +import org.keycloak.provider.ProviderFactory; + +/** + * @author Stian Thorgersen + */ +public interface WellKnownProviderFactory extends ProviderFactory { + +} diff --git a/services/src/main/java/org/keycloak/wellknown/WellKnownSpi.java b/services/src/main/java/org/keycloak/wellknown/WellKnownSpi.java new file mode 100755 index 0000000000..7cb962d7f9 --- /dev/null +++ b/services/src/main/java/org/keycloak/wellknown/WellKnownSpi.java @@ -0,0 +1,27 @@ +package org.keycloak.wellknown; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Stian Thorgersen + */ +public class WellKnownSpi implements Spi { + + @Override + public String getName() { + return "well-known"; + } + + @Override + public Class getProviderClass() { + return WellKnownProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return WellKnownProviderFactory.class; + } + +} diff --git a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 30eeb3e59b..cb0145565e 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -1,3 +1,4 @@ org.keycloak.protocol.LoginProtocolSpi org.keycloak.protocol.ProtocolMapperSpi -org.keycloak.exportimport.ApplicationImportSpi \ No newline at end of file +org.keycloak.exportimport.ApplicationImportSpi +org.keycloak.wellknown.WellKnownSpi \ No newline at end of file diff --git a/services/src/main/resources/META-INF/services/org.keycloak.wellknown.WellKnownProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.wellknown.WellKnownProviderFactory new file mode 100644 index 0000000000..b0a54e2e4d --- /dev/null +++ b/services/src/main/resources/META-INF/services/org.keycloak.wellknown.WellKnownProviderFactory @@ -0,0 +1 @@ +org.keycloak.protocol.oidc.OIDCWellKnownProviderFactory \ No newline at end of file diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java index cfe16a8e76..e1d83b32a3 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java @@ -429,7 +429,7 @@ public class AdapterTestStrategy extends ExternalResource { Response response = target.request() .header(HttpHeaders.AUTHORIZATION, header) .post(Entity.form(form)); - Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(401, response.getStatus()); response.close(); client.close(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java index b6f45bd294..56538a738c 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java @@ -436,7 +436,19 @@ public class AccessTokenTest { Response response = grantTarget.request() .header(HttpHeaders.AUTHORIZATION, header) .post(Entity.form(form)); - Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(401, response.getStatus()); + response.close(); + } + + { // test invalid password + String header = BasicAuthHelper.createHeader("test-app", "password"); + Form form = new Form(); + form.param("username", "test-user@localhost"); + form.param("password", "invalid"); + Response response = grantTarget.request() + .header(HttpHeaders.AUTHORIZATION, header) + .post(Entity.form(form)); + Assert.assertEquals(401, response.getStatus()); response.close(); } @@ -477,7 +489,7 @@ public class AccessTokenTest { } Response response = executeGrantAccessTokenRequest(grantTarget); - Assert.assertEquals(401, response.getStatus()); + Assert.assertEquals(403, response.getStatus()); response.close(); { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java index efd54fe085..c75a030821 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java @@ -108,7 +108,7 @@ public class OAuthRedirectUriTest { oauth.openLoginForm(); Assert.assertTrue(errorPage.isCurrent()); - Assert.assertEquals("Invalid redirect_uri.", errorPage.getError()); + Assert.assertEquals("Invalid redirect_uri", errorPage.getError()); } finally { keycloakRule.update(new KeycloakRule.KeycloakSetup() { @Override @@ -133,7 +133,7 @@ public class OAuthRedirectUriTest { oauth.openLoginForm(); Assert.assertTrue(errorPage.isCurrent()); - Assert.assertEquals("Invalid redirect_uri.", errorPage.getError()); + Assert.assertEquals("Invalid redirect_uri", errorPage.getError()); } finally { keycloakRule.update(new KeycloakRule.KeycloakSetup() { @Override @@ -158,7 +158,7 @@ public class OAuthRedirectUriTest { oauth.openLoginForm(); Assert.assertTrue(errorPage.isCurrent()); - Assert.assertEquals("Invalid redirect_uri.", errorPage.getError()); + Assert.assertEquals("Invalid redirect_uri", errorPage.getError()); } finally { keycloakRule.update(new KeycloakRule.KeycloakSetup() { @Override @@ -184,7 +184,7 @@ public class OAuthRedirectUriTest { oauth.openLoginForm(); Assert.assertTrue(errorPage.isCurrent()); - Assert.assertEquals("Invalid redirect_uri.", errorPage.getError()); + Assert.assertEquals("Invalid redirect_uri", errorPage.getError()); } @Test @@ -244,7 +244,7 @@ public class OAuthRedirectUriTest { Assert.assertTrue(loginPage.isCurrent()); } else { Assert.assertTrue(errorPage.isCurrent()); - Assert.assertEquals("Invalid redirect_uri.", errorPage.getError()); + Assert.assertEquals("Invalid redirect_uri", errorPage.getError()); } if (expectValid) { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java index f172dec304..ea269c339d 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java @@ -195,7 +195,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest { OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "test-user@localhost", "invalid"); - assertEquals(400, response.getStatusCode()); + assertEquals(401, response.getStatusCode()); assertEquals("invalid_grant", response.getError()); @@ -216,7 +216,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest { OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "invalid", "invalid"); - assertEquals(400, response.getStatusCode()); + assertEquals(401, response.getStatusCode()); assertEquals("invalid_grant", response.getError());