keycloak-aplcache
Changes
core/pom.xml 6(+6 -0)
services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java 91(+91 -0)
services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java 123(+123 -0)
services/src/main/resources/META-INF/services/org.keycloak.wellknown.WellKnownProviderFactory 1(+1 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java 2(+1 -1)
Details
core/pom.xml 6(+6 -0)
diff --git a/core/pom.xml b/core/pom.xml
index 5754ce3..3efc75a 100755
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -56,6 +56,12 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.nimbusds</groupId>
+ <artifactId>nimbus-jose-jwt</artifactId>
+ <version>3.9</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
<resources>
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 0000000..d292f41
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 0000000..bc3a228
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 0000000..38f02d8
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class JWKParser {
+
+ private static TypeReference<Map<String,String>> typeRef = new TypeReference<Map<String,String>>() {};
+
+ private Map<String, String> 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 0000000..8090599
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index 07071ff..5aba901 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/util/JsonSerialization.java b/core/src/main/java/org/keycloak/util/JsonSerialization.java
index a1a93ba..ff080de 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 0000000..7b6a861
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 f6531eb..7a4404d 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 080b7e8..a6a2330 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/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
new file mode 100644
index 0000000..6f6441f
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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<String, String> 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<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
+ for (IdentityProviderModel identityProvider : identityProviders) {
+ if (identityProvider.isAuthenticateByDefault()) {
+ return buildRedirectToIdentityProvider(identityProvider.getId(), accessCode);
+ }
+ }
+
+ List<RequiredCredentialModel> 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<String, String> formData = new MultivaluedMapImpl<String, String>();
+
+ 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 0000000..30359d1
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 0000000..16ab80c
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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<String, String> 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<String, String> 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 0000000..ab78e6c
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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/endpoints/ValidateTokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/ValidateTokenEndpoint.java
new file mode 100644
index 0000000..caef436
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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<String, String> err = new HashMap<String, String>();
+ 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<String, String> error = new HashMap<String, String>();
+ 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/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
index 9258264..3900f1a 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 3de9a87..c2ec74b 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
- */
- @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);
- }
- }
-
-
- /**
- * 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 <a href="http://tools.ietf.org/html/rfc6749#section-4.3">http://tools.ietf.org/html/rfc6749#section-4.3</a>
- *
- * @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<String, String> 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<String, String> err;
-
- switch (authenticationStatus) {
- case SUCCESS:
- break;
- case ACCOUNT_TEMPORARILY_DISABLED:
- case ACTIONS_REQUIRED:
- err = new HashMap<String, String>();
- 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<String, String>();
- 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<String, String>();
- 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();
- }
-
- /**
- * Validate encoded access token.
- *
- * @param tokenString
- * @return Unmarshalled token
- */
- @Path("validate")
- @GET
- @NoCache
- @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<String, String> err = new HashMap<String, String>();
- 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<String, String> error = new HashMap<String, String>();
- 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
+ * Authorization endpoint
*/
- @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();
+ @Path("auth")
+ public Object auth() {
+ AuthorizationEndpoint endpoint = new AuthorizationEndpoint(authManager, realm, event);
+ ResteasyProviderFactory.getInstance().injectProperties(endpoint);
+ return endpoint.init();
}
/**
- * URL for making refresh token requests.
- *
- * @See <a href="http://tools.ietf.org/html/rfc6749#section-6">http://tools.ietf.org/html/rfc6749#section-6</a>
- *
- * 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
+ * Registration endpoint
*/
- @Path("refresh")
- @POST
- @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
- @Produces(MediaType.APPLICATION_JSON)
- public Response refreshAccessToken(final @HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader,
- final MultivaluedMap<String, String> 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<String, String> error = new HashMap<String, String>();
- 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<String, String> error = new HashMap<String, String>();
- 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();
+ @Path("registrations")
+ public Object registerPage() {
+ AuthorizationEndpoint endpoint = new AuthorizationEndpoint(authManager, realm, event);
+ ResteasyProviderFactory.getInstance().injectProperties(endpoint);
+ return endpoint.register().init();
}
/**
- * CORS preflight path for access code to token
- *
- * @return
+ * Token endpoint
*/
- @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();
+ @Path("token")
+ public Object token() {
+ TokenEndpoint endpoint = new TokenEndpoint(tokenManager, authManager, realm, event);
+ ResteasyProviderFactory.getInstance().injectProperties(endpoint);
+ return endpoint.init();
}
- /**
- * URL invoked by adapter to turn an access code to access token
- *
- * @See <a href="http://tools.ietf.org/html/rfc6749#section-4.1">http://tools.ietf.org/html/rfc6749#section-4.1</a>
- *
- * @param authorizationHeader
- * @param formData
- * @return
- */
- @Path("access/codes")
- @POST
- @Produces("application/json")
- public Response accessCodeToToken(@HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader, final MultivaluedMap<String, String> 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<String, String> error = new HashMap<String, String>();
- 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<String, String> res = new HashMap<String, String>();
- 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<String, String> res = new HashMap<String, String>();
- 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<String, String> res = new HashMap<String, String>();
- 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<String, String> res = new HashMap<String, String>();
- 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<String, String> res = new HashMap<String, String>();
- 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<String, String> res = new HashMap<String, String>();
- 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<String, String> res = new HashMap<String, String>();
- 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();
+ @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("userinfo")
- public Object issueUserInfo() {
- UserInfoService userInfoEndpoint = new UserInfoService(this);
-
- ResteasyProviderFactory.getInstance().injectProperties(userInfoEndpoint);
-
- return userInfoEndpoint;
+ @Path("login-status-iframe.html")
+ public Object getLoginStatusIframe() {
+ LoginStatusIframeEndpoint endpoint = new LoginStatusIframeEndpoint(realm);
+ ResteasyProviderFactory.getInstance().injectProperties(endpoint);
+ return endpoint;
}
- protected ClientModel authorizeClient(String authorizationHeader, MultivaluedMap<String, String> formData, EventBuilder event) {
- ClientModel client = authorizeClientBase(authorizationHeader, formData, event, realm);
-
- if ( (client instanceof ApplicationModel) && ((ApplicationModel)client).isBearerOnly()) {
- Map<String, String> error = new HashMap<String, String>();
- 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;
+ @Path("grants/access")
+ @Deprecated
+ public Object grantAccessToken() {
+ TokenEndpoint endpoint = new TokenEndpoint(tokenManager, authManager, realm, event);
+ ResteasyProviderFactory.getInstance().injectProperties(endpoint);
+ return endpoint.legacy(OAuth2Constants.PASSWORD).init();
}
- // Just authorize client without further checking about client type
- public static ClientModel authorizeClientBase(String authorizationHeader, MultivaluedMap<String, String> 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<String, String> error = new HashMap<String, String>();
- 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<String, String> error = new HashMap<String, String>();
- 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<String, String> error = new HashMap<String, String>();
- 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<String, String> error = new HashMap<String, String>();
- 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;
+ @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();
}
- /**
- * 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;
- }
+ @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();
}
- /**
- * Login page. Must be redirected to from the application or oauth client.
- *
- * @See <a href="http://tools.ietf.org/html/rfc6749#section-4.1">http://tools.ietf.org/html/rfc6749#section-4.1</a>
- *
- *
- * @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<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
- for (IdentityProviderModel identityProvider : identityProviders) {
- if (identityProvider.isAuthenticateByDefault()) {
- return redirectToIdentityProvider(identityProvider.getId(), accessCode);
- }
- }
-
- List<RequiredCredentialModel> 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<String, String> formData = new MultivaluedMapImpl<String, String>();
-
- if (loginHint != null) {
- formData.add(AuthenticationManager.FORM_USERNAME, loginHint);
- } else {
- formData.add(AuthenticationManager.FORM_USERNAME, rememberMeUsername);
- formData.add("rememberMe", "on");
- }
-
- forms.setFormData(formData);
- }
+ @Path("validate")
+ public Object validateAccessToken(@QueryParam("access_token") String tokenString) {
+ ValidateTokenEndpoint endpoint = new ValidateTokenEndpoint(tokenManager, realm, event);
+ ResteasyProviderFactory.getInstance().injectProperties(endpoint);
+ return endpoint;
- 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();
+ @Path("certs")
+ @Produces(MediaType.APPLICATION_JSON)
+ public JSONWebKeySet certs() {
+ JSONWebKeySet keySet = new JSONWebKeySet();
+ keySet.setKeys(new JWK[]{JWKBuilder.create().rs256(realm.getPublicKey())});
+ return keySet;
}
- /**
- * 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();
- }
+ @Path("userinfo")
+ public Object issueUserInfo() {
+ UserInfoEndpoint endpoint = new UserInfoEndpoint(tokenManager, realm);
+ ResteasyProviderFactory.getInstance().injectProperties(endpoint);
+ return endpoint;
}
- /**
- * 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<String, String> 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<String, String> error = new HashMap<String, String>();
- 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<String, String> error = new HashMap<String, String>();
- 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<String> 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<String> getValidateRedirectUris(RealmModel realm) {
- Set<String> redirects = new HashSet<String>();
- 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<String> validRedirects = getValidateRedirectUris(realm);
- return verifyRedirectUri(uriInfo, redirectUri, realm, validRedirects);
- }
-
- public static String verifyRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm, ClientModel client) {
- Set<String> validRedirects = client.getRedirectUris();
- return verifyRedirectUri(uriInfo, redirectUri, realm, validRedirects);
- }
-
- public static String verifyRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm, Set<String> 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<String> 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<String> resolveValidRedirects(UriInfo uriInfo, Set<String> validRedirects) {
- // If the valid redirect URI is relative (no scheme, host, port) then use the request's scheme, host, and port
- Set<String> resolveValidRedirects = new HashSet<String>();
- 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<String, String> e = new HashMap<String, String>();
- 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 0000000..54e4009
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class OIDCWellKnownProvider implements WellKnownProvider {
+
+ public static final List<String> DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = list("RS256");
+
+ public static final List<String> DEFAULT_GRANT_TYPES_SUPPORTED = list(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN);
+
+ public static final List<String> DEFAULT_RESPONSE_TYPES_SUPPORTED = list(OAuth2Constants.CODE);
+
+ public static final List<String> DEFAULT_SUBJECT_TYPES_SUPPORTED = list("public");
+
+ public static final List<String> 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<String> grantTypes = new LinkedList<>(DEFAULT_GRANT_TYPES_SUPPORTED);
+ grantTypes.add(OAuth2Constants.PASSWORD);
+ config.setGrantTypesSupported(grantTypes);
+ }
+
+ return config;
+ }
+
+ @Override
+ public void close() {
+ }
+
+ private static List<String> list(String... values) {
+ List<String> 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 0000000..e49a993
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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/representations/JSONWebKeySet.java b/services/src/main/java/org/keycloak/protocol/oidc/representations/JSONWebKeySet.java
new file mode 100644
index 0000000..3ac9547
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 0000000..0760b64
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+
+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<String> grantTypesSupported;
+
+ @JsonProperty("response_types_supported")
+ private List<String> responseTypesSupported;
+
+ @JsonProperty("subject_types_supported")
+ private List<String> subjectTypesSupported;
+
+ @JsonProperty("id_token_signing_alg_values_supported")
+ private List<String> idTokenSigningAlgValuesSupported;
+
+ @JsonProperty("response_modes_supported")
+ private List<String> 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<String> getGrantTypesSupported() {
+ return grantTypesSupported;
+ }
+
+ public void setGrantTypesSupported(List<String> grantTypesSupported) {
+ this.grantTypesSupported = grantTypesSupported;
+ }
+
+ public List<String> getResponseTypesSupported() {
+ return responseTypesSupported;
+ }
+
+ public void setResponseTypesSupported(List<String> responseTypesSupported) {
+ this.responseTypesSupported = responseTypesSupported;
+ }
+
+ public List<String> getSubjectTypesSupported() {
+ return subjectTypesSupported;
+ }
+
+ public void setSubjectTypesSupported(List<String> subjectTypesSupported) {
+ this.subjectTypesSupported = subjectTypesSupported;
+ }
+
+ public List<String> getIdTokenSigningAlgValuesSupported() {
+ return idTokenSigningAlgValuesSupported;
+ }
+
+ public void setIdTokenSigningAlgValuesSupported(List<String> idTokenSigningAlgValuesSupported) {
+ this.idTokenSigningAlgValuesSupported = idTokenSigningAlgValuesSupported;
+ }
+
+ public List<String> getResponseModesSupported() {
+ return responseModesSupported;
+ }
+
+ public void setResponseModesSupported(List<String> 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 0000000..1a78b64
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class AuthorizeClientUtil {
+
+ public static ClientModel authorizeClient(String authorizationHeader, MultivaluedMap<String, String> 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<String, String> error = new HashMap<String, String>();
+ 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<String, String> error = new HashMap<String, String>();
+ 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<String, String> error = new HashMap<String, String>();
+ 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<String, String> error = new HashMap<String, String>();
+ 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 0000000..2fa3aee
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class RedirectUtils {
+
+ private static final Logger logger = Logger.getLogger(RedirectUtils.class);
+
+ public static String verifyRealmRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm) {
+ Set<String> validRedirects = getValidateRedirectUris(realm);
+ return verifyRedirectUri(uriInfo, redirectUri, realm, validRedirects);
+ }
+
+ public static String verifyRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm, ClientModel client) {
+ Set<String> validRedirects = client.getRedirectUris();
+ return verifyRedirectUri(uriInfo, redirectUri, realm, validRedirects);
+ }
+
+ public static Set<String> resolveValidRedirects(UriInfo uriInfo, Set<String> validRedirects) {
+ // If the valid redirect URI is relative (no scheme, host, port) then use the request's scheme, host, and port
+ Set<String> resolveValidRedirects = new HashSet<String>();
+ 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<String> getValidateRedirectUris(RealmModel realm) {
+ Set<String> redirects = new HashSet<String>();
+ 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<String> 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<String> 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<String> 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 0000000..3f8d435
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 0000000..bf9f278
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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<String, String> e = new HashMap<String, String>();
+ 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 d286a14..8b5297b 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/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index e508a70..5732688 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/services/resources/ClientsManagementService.java b/services/src/main/java/org/keycloak/services/resources/ClientsManagementService.java
index b49058f..1761138 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<String, String> formData) {
- ClientModel client = OIDCLoginProtocolService.authorizeClientBase(authorizationHeader, formData, event, realm);
+ ClientModel client = AuthorizeClientUtil.authorizeClient(authorizationHeader, formData, event, realm);
if (client.isPublicClient()) {
Map<String, String> error = new HashMap<String, String>();
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 8a50c69..247a430 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/wellknown/WellKnownProvider.java b/services/src/main/java/org/keycloak/wellknown/WellKnownProvider.java
new file mode 100755
index 0000000..d4b80d7
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 0000000..21d8b81
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface WellKnownProviderFactory extends ProviderFactory<WellKnownProvider> {
+
+}
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 0000000..7cb962d
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class WellKnownSpi implements Spi {
+
+ @Override
+ public String getName() {
+ return "well-known";
+ }
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return WellKnownProvider.class;
+ }
+
+ @Override
+ public Class<? extends ProviderFactory> 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 30eeb3e..cb01455 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 0000000..b0a54e2
--- /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 cfe16a8..e1d83b3 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 b6f45bd..56538a7 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 efd54fe..c75a030 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 f172dec..ea269c3 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());