[KEYCLOAK-8575] oidc idp basic auth (#6268)
* [KEYCLOAK-8575] Allow to choose between basic auth and form auth for oidc idp * uncomment ui and add tests * move basic auth to abstract identity provider (except for getting refresh tokens) * removed duplications
This commit is contained in:
parent
b833ce9dd3
commit
b71198af9f
7 changed files with 212 additions and 126 deletions
|
@ -27,6 +27,7 @@ import org.apache.http.HttpResponse;
|
||||||
import org.apache.http.NameValuePair;
|
import org.apache.http.NameValuePair;
|
||||||
import org.apache.http.client.HttpClient;
|
import org.apache.http.client.HttpClient;
|
||||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||||
|
import org.apache.http.client.methods.HttpDelete;
|
||||||
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
|
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
|
||||||
import org.apache.http.client.methods.HttpGet;
|
import org.apache.http.client.methods.HttpGet;
|
||||||
import org.apache.http.client.methods.HttpPost;
|
import org.apache.http.client.methods.HttpPost;
|
||||||
|
@ -35,6 +36,7 @@ import org.apache.http.client.methods.HttpRequestBase;
|
||||||
import org.apache.http.client.utils.URIBuilder;
|
import org.apache.http.client.utils.URIBuilder;
|
||||||
import org.apache.http.entity.StringEntity;
|
import org.apache.http.entity.StringEntity;
|
||||||
import org.apache.http.message.BasicNameValuePair;
|
import org.apache.http.message.BasicNameValuePair;
|
||||||
|
import org.keycloak.common.util.Base64;
|
||||||
import org.keycloak.connections.httpclient.HttpClientProvider;
|
import org.keycloak.connections.httpclient.HttpClientProvider;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
@ -46,15 +48,12 @@ import java.io.StringWriter;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import java.util.zip.GZIPInputStream;
|
import java.util.zip.GZIPInputStream;
|
||||||
import org.apache.http.client.methods.HttpDelete;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
@ -133,6 +132,12 @@ public class SimpleHttp {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SimpleHttp authBasic(final String username, final String password) {
|
||||||
|
final String basicCredentials = String.format("%s:%s", username, password);
|
||||||
|
header("Authorization", "Basic " + Base64.encodeBytes(basicCredentials.getBytes()));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public SimpleHttp acceptJson() {
|
public SimpleHttp acceptJson() {
|
||||||
if (headers == null || !headers.containsKey("Accept")) {
|
if (headers == null || !headers.containsKey("Accept")) {
|
||||||
header("Accept", "application/json");
|
header("Accept", "application/json");
|
||||||
|
|
|
@ -25,8 +25,8 @@ import org.keycloak.broker.provider.AbstractIdentityProvider;
|
||||||
import org.keycloak.broker.provider.AuthenticationRequest;
|
import org.keycloak.broker.provider.AuthenticationRequest;
|
||||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||||
import org.keycloak.broker.provider.ExchangeExternalToken;
|
import org.keycloak.broker.provider.ExchangeExternalToken;
|
||||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
|
||||||
import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
|
import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
|
||||||
|
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||||
import org.keycloak.broker.provider.IdentityProvider;
|
import org.keycloak.broker.provider.IdentityProvider;
|
||||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||||
import org.keycloak.common.ClientConnection;
|
import org.keycloak.common.ClientConnection;
|
||||||
|
@ -47,10 +47,10 @@ import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.representations.AccessTokenResponse;
|
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
|
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
|
||||||
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
import org.keycloak.representations.JsonWebToken;
|
import org.keycloak.representations.JsonWebToken;
|
||||||
import org.keycloak.services.ErrorPage;
|
import org.keycloak.services.ErrorPage;
|
||||||
import org.keycloak.services.ErrorResponseException;
|
import org.keycloak.services.ErrorResponseException;
|
||||||
|
@ -465,6 +465,9 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
||||||
.param(OAuth2Constants.CLIENT_ASSERTION, jws);
|
.param(OAuth2Constants.CLIENT_ASSERTION, jws);
|
||||||
} else {
|
} else {
|
||||||
try (VaultStringSecret vaultStringSecret = session.vault().getStringSecret(getConfig().getClientSecret())) {
|
try (VaultStringSecret vaultStringSecret = session.vault().getStringSecret(getConfig().getClientSecret())) {
|
||||||
|
if (getConfig().isBasicAuthentication()) {
|
||||||
|
return tokenRequest.authBasic(getConfig().getClientId(), vaultStringSecret.get().orElse(getConfig().getClientSecret()));
|
||||||
|
}
|
||||||
return tokenRequest
|
return tokenRequest
|
||||||
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
|
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
|
||||||
.param(OAUTH2_PARAMETER_CLIENT_SECRET, vaultStringSecret.get().orElse(getConfig().getClientSecret()));
|
.param(OAUTH2_PARAMETER_CLIENT_SECRET, vaultStringSecret.get().orElse(getConfig().getClientSecret()));
|
||||||
|
|
|
@ -100,6 +100,10 @@ public class OAuth2IdentityProviderConfig extends IdentityProviderModel {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isBasicAuthentication(){
|
||||||
|
return getClientAuthMethod().equals(OIDCLoginProtocol.CLIENT_SECRET_BASIC);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isUiLocales() {
|
public boolean isUiLocales() {
|
||||||
return Boolean.valueOf(getConfig().get("uiLocales"));
|
return Boolean.valueOf(getConfig().get("uiLocales"));
|
||||||
}
|
}
|
||||||
|
@ -119,4 +123,4 @@ public class OAuth2IdentityProviderConfig extends IdentityProviderModel {
|
||||||
public void setForwardParameters(String forwardParameters) {
|
public void setForwardParameters(String forwardParameters) {
|
||||||
getConfig().put("forwardParameters", forwardParameters);
|
getConfig().put("forwardParameters", forwardParameters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.representations.AccessTokenResponse;
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
import org.keycloak.representations.JsonWebToken;
|
import org.keycloak.representations.JsonWebToken;
|
||||||
|
@ -94,41 +95,20 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
return new OIDCEndpoint(callback, realm, event);
|
return new OIDCEndpoint(callback, realm, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected class OIDCEndpoint extends Endpoint {
|
/**
|
||||||
public OIDCEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) {
|
* Returns access token response as a string from a refresh token invocation on the remote OIDC broker
|
||||||
super(callback, realm, event);
|
*
|
||||||
|
* @param session
|
||||||
|
* @param userSession
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String refreshTokenForLogout(KeycloakSession session, UserSessionModel userSession) {
|
||||||
|
String refreshToken = userSession.getNote(FEDERATED_REFRESH_TOKEN);
|
||||||
|
try (VaultStringSecret vaultStringSecret = session.vault().getStringSecret(getConfig().getClientSecret())) {
|
||||||
|
return getRefreshTokenRequest(session, refreshToken, getConfig().getClientId(), vaultStringSecret.get().orElse(getConfig().getClientSecret())).asString();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("logout_response")
|
|
||||||
public Response logoutResponse(@QueryParam("state") String state) {
|
|
||||||
if (state == null){
|
|
||||||
logger.error("no state parameter returned");
|
|
||||||
EventBuilder event = new EventBuilder(realm, session, clientConnection);
|
|
||||||
event.event(EventType.LOGOUT);
|
|
||||||
event.error(Errors.USER_SESSION_NOT_FOUND);
|
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
|
||||||
|
|
||||||
}
|
|
||||||
UserSessionModel userSession = session.sessions().getUserSession(realm, state);
|
|
||||||
if (userSession == null) {
|
|
||||||
logger.error("no valid user session");
|
|
||||||
EventBuilder event = new EventBuilder(realm, session, clientConnection);
|
|
||||||
event.event(EventType.LOGOUT);
|
|
||||||
event.error(Errors.USER_SESSION_NOT_FOUND);
|
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
|
||||||
}
|
|
||||||
if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) {
|
|
||||||
logger.error("usersession in different state");
|
|
||||||
EventBuilder event = new EventBuilder(realm, session, clientConnection);
|
|
||||||
event.event(EventType.LOGOUT);
|
|
||||||
event.error(Errors.USER_SESSION_NOT_FOUND);
|
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.SESSION_NOT_ACTIVE);
|
|
||||||
}
|
|
||||||
return AuthenticationManager.finishBrowserLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -180,21 +160,66 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Returns access token response as a string from a refresh token invocation on the remote OIDC broker
|
protected Response exchangeStoredToken(UriInfo uriInfo, EventBuilder event, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
|
||||||
*
|
FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
|
||||||
* @param session
|
if (model == null || model.getToken() == null) {
|
||||||
* @param userSession
|
event.detail(Details.REASON, "requested_issuer is not linked");
|
||||||
* @return
|
event.error(Errors.INVALID_TOKEN);
|
||||||
*/
|
return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
|
||||||
public String refreshTokenForLogout(KeycloakSession session, UserSessionModel userSession) {
|
}
|
||||||
String refreshToken = userSession.getNote(FEDERATED_REFRESH_TOKEN);
|
|
||||||
try (VaultStringSecret vaultStringSecret = session.vault().getStringSecret(getConfig().getClientSecret())) {
|
try (VaultStringSecret vaultStringSecret = session.vault().getStringSecret(getConfig().getClientSecret())) {
|
||||||
return SimpleHttp.doPost(getConfig().getTokenUrl(), session)
|
String modelTokenString = model.getToken();
|
||||||
.param("refresh_token", refreshToken)
|
AccessTokenResponse tokenResponse = JsonSerialization.readValue(modelTokenString, AccessTokenResponse.class);
|
||||||
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_REFRESH_TOKEN)
|
Integer exp = (Integer) tokenResponse.getOtherClaims().get(ACCESS_TOKEN_EXPIRATION);
|
||||||
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
|
if (exp != null && exp < Time.currentTime()) {
|
||||||
.param(OAUTH2_PARAMETER_CLIENT_SECRET, vaultStringSecret.get().orElse(getConfig().getClientSecret())).asString();
|
if (tokenResponse.getRefreshToken() == null) {
|
||||||
|
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
|
||||||
|
}
|
||||||
|
String response = getRefreshTokenRequest(session, tokenResponse.getRefreshToken(),
|
||||||
|
getConfig().getClientId(), vaultStringSecret.get().orElse(getConfig().getClientSecret())).asString();
|
||||||
|
if (response.contains("error")) {
|
||||||
|
logger.debugv("Error refreshing token, refresh token expiration?: {0}", response);
|
||||||
|
model.setToken(null);
|
||||||
|
session.users().updateFederatedIdentity(authorizedClient.getRealm(), tokenSubject, model);
|
||||||
|
event.detail(Details.REASON, "requested_issuer token expired");
|
||||||
|
event.error(Errors.INVALID_TOKEN);
|
||||||
|
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
|
||||||
|
}
|
||||||
|
AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
|
||||||
|
if (newResponse.getExpiresIn() > 0) {
|
||||||
|
int accessTokenExpiration = Time.currentTime() + (int) newResponse.getExpiresIn();
|
||||||
|
newResponse.getOtherClaims().put(ACCESS_TOKEN_EXPIRATION, accessTokenExpiration);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newResponse.getRefreshToken() == null && tokenResponse.getRefreshToken() != null) {
|
||||||
|
newResponse.setRefreshToken(tokenResponse.getRefreshToken());
|
||||||
|
newResponse.setRefreshExpiresIn(tokenResponse.getRefreshExpiresIn());
|
||||||
|
}
|
||||||
|
response = JsonSerialization.writeValueAsString(newResponse);
|
||||||
|
|
||||||
|
String oldToken = tokenUserSession.getNote(FEDERATED_ACCESS_TOKEN);
|
||||||
|
if (oldToken != null && oldToken.equals(tokenResponse.getToken())) {
|
||||||
|
int accessTokenExpiration = newResponse.getExpiresIn() > 0 ? Time.currentTime() + (int) newResponse.getExpiresIn() : 0;
|
||||||
|
tokenUserSession.setNote(FEDERATED_TOKEN_EXPIRATION, Long.toString(accessTokenExpiration));
|
||||||
|
tokenUserSession.setNote(FEDERATED_REFRESH_TOKEN, newResponse.getRefreshToken());
|
||||||
|
tokenUserSession.setNote(FEDERATED_ACCESS_TOKEN, newResponse.getToken());
|
||||||
|
tokenUserSession.setNote(FEDERATED_ID_TOKEN, newResponse.getIdToken());
|
||||||
|
|
||||||
|
}
|
||||||
|
model.setToken(response);
|
||||||
|
tokenResponse = newResponse;
|
||||||
|
} else if (exp != null) {
|
||||||
|
tokenResponse.setExpiresIn(exp - Time.currentTime());
|
||||||
|
}
|
||||||
|
tokenResponse.setIdToken(null);
|
||||||
|
tokenResponse.setRefreshToken(null);
|
||||||
|
tokenResponse.setRefreshExpiresIn(0);
|
||||||
|
tokenResponse.getOtherClaims().clear();
|
||||||
|
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
|
||||||
|
tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
|
||||||
|
event.success();
|
||||||
|
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
@ -224,72 +249,18 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
protected SimpleHttp getRefreshTokenRequest(KeycloakSession session, String refreshToken, String clientId, String clientSecret) {
|
||||||
protected Response exchangeStoredToken(UriInfo uriInfo, EventBuilder event, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
|
if (OIDCLoginProtocol.CLIENT_SECRET_BASIC.equals(getConfig().getClientAuthMethod())) {
|
||||||
FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
|
return SimpleHttp.doPost(getConfig().getTokenUrl(), session)
|
||||||
if (model == null || model.getToken() == null) {
|
.param("refresh_token", refreshToken)
|
||||||
event.detail(Details.REASON, "requested_issuer is not linked");
|
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_REFRESH_TOKEN)
|
||||||
event.error(Errors.INVALID_TOKEN);
|
.authBasic(clientId, clientSecret);
|
||||||
return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
|
|
||||||
}
|
|
||||||
try (VaultStringSecret vaultStringSecret = session.vault().getStringSecret(getConfig().getClientSecret())) {
|
|
||||||
String modelTokenString = model.getToken();
|
|
||||||
AccessTokenResponse tokenResponse = JsonSerialization.readValue(modelTokenString, AccessTokenResponse.class);
|
|
||||||
Integer exp = (Integer) tokenResponse.getOtherClaims().get(ACCESS_TOKEN_EXPIRATION);
|
|
||||||
if (exp != null && exp < Time.currentTime()) {
|
|
||||||
if (tokenResponse.getRefreshToken() == null) {
|
|
||||||
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
|
|
||||||
}
|
|
||||||
String response = SimpleHttp.doPost(getConfig().getTokenUrl(), session)
|
|
||||||
.param("refresh_token", tokenResponse.getRefreshToken())
|
|
||||||
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_REFRESH_TOKEN)
|
|
||||||
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
|
|
||||||
.param(OAUTH2_PARAMETER_CLIENT_SECRET, vaultStringSecret.get().orElse(getConfig().getClientSecret())).asString();
|
|
||||||
if (response.contains("error")) {
|
|
||||||
logger.debugv("Error refreshing token, refresh token expiration?: {0}", response);
|
|
||||||
model.setToken(null);
|
|
||||||
session.users().updateFederatedIdentity(authorizedClient.getRealm(), tokenSubject, model);
|
|
||||||
event.detail(Details.REASON, "requested_issuer token expired");
|
|
||||||
event.error(Errors.INVALID_TOKEN);
|
|
||||||
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
|
|
||||||
}
|
|
||||||
AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
|
|
||||||
if (newResponse.getExpiresIn() > 0) {
|
|
||||||
int accessTokenExpiration = Time.currentTime() + (int) newResponse.getExpiresIn();
|
|
||||||
newResponse.getOtherClaims().put(ACCESS_TOKEN_EXPIRATION, accessTokenExpiration);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newResponse.getRefreshToken() == null && tokenResponse.getRefreshToken() != null) {
|
|
||||||
newResponse.setRefreshToken(tokenResponse.getRefreshToken());
|
|
||||||
newResponse.setRefreshExpiresIn(tokenResponse.getRefreshExpiresIn());
|
|
||||||
}
|
|
||||||
response = JsonSerialization.writeValueAsString(newResponse);
|
|
||||||
|
|
||||||
String oldToken = tokenUserSession.getNote(FEDERATED_ACCESS_TOKEN);
|
|
||||||
if (oldToken != null && oldToken.equals(tokenResponse.getToken())) {
|
|
||||||
int accessTokenExpiration = newResponse.getExpiresIn() > 0 ? Time.currentTime() + (int) newResponse.getExpiresIn() : 0;
|
|
||||||
tokenUserSession.setNote(FEDERATED_TOKEN_EXPIRATION, Long.toString(accessTokenExpiration));
|
|
||||||
tokenUserSession.setNote(FEDERATED_REFRESH_TOKEN, newResponse.getRefreshToken());
|
|
||||||
tokenUserSession.setNote(FEDERATED_ACCESS_TOKEN, newResponse.getToken());
|
|
||||||
tokenUserSession.setNote(FEDERATED_ID_TOKEN, newResponse.getIdToken());
|
|
||||||
|
|
||||||
}
|
|
||||||
model.setToken(response);
|
|
||||||
tokenResponse = newResponse;
|
|
||||||
} else if (exp != null) {
|
|
||||||
tokenResponse.setExpiresIn(exp - Time.currentTime());
|
|
||||||
}
|
|
||||||
tokenResponse.setIdToken(null);
|
|
||||||
tokenResponse.setRefreshToken(null);
|
|
||||||
tokenResponse.setRefreshExpiresIn(0);
|
|
||||||
tokenResponse.getOtherClaims().clear();
|
|
||||||
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
|
|
||||||
tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
|
|
||||||
event.success();
|
|
||||||
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
}
|
||||||
|
return SimpleHttp.doPost(getConfig().getTokenUrl(), session)
|
||||||
|
.param("refresh_token", refreshToken)
|
||||||
|
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_REFRESH_TOKEN)
|
||||||
|
.param(OAUTH2_PARAMETER_CLIENT_ID, clientId)
|
||||||
|
.param(OAUTH2_PARAMETER_CLIENT_SECRET, clientSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -317,11 +288,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
event.success();
|
event.success();
|
||||||
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
}
|
}
|
||||||
String response = SimpleHttp.doPost(getConfig().getTokenUrl(), session)
|
String response = getRefreshTokenRequest(session, refreshToken, getConfig().getClientId(), vaultStringSecret.get().orElse(getConfig().getClientSecret())).asString();
|
||||||
.param("refresh_token", refreshToken)
|
|
||||||
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_REFRESH_TOKEN)
|
|
||||||
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
|
|
||||||
.param(OAUTH2_PARAMETER_CLIENT_SECRET, vaultStringSecret.get().orElse(getConfig().getClientSecret())).asString();
|
|
||||||
if (response.contains("error")) {
|
if (response.contains("error")) {
|
||||||
logger.debugv("Error refreshing token, refresh token expiration?: {0}", response);
|
logger.debugv("Error refreshing token, refresh token expiration?: {0}", response);
|
||||||
event.detail(Details.REASON, "requested_issuer token expired");
|
event.detail(Details.REASON, "requested_issuer token expired");
|
||||||
|
@ -347,6 +314,42 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected class OIDCEndpoint extends Endpoint {
|
||||||
|
public OIDCEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) {
|
||||||
|
super(callback, realm, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("logout_response")
|
||||||
|
public Response logoutResponse(@QueryParam("state") String state) {
|
||||||
|
if (state == null){
|
||||||
|
logger.error("no state parameter returned");
|
||||||
|
EventBuilder event = new EventBuilder(realm, session, clientConnection);
|
||||||
|
event.event(EventType.LOGOUT);
|
||||||
|
event.error(Errors.USER_SESSION_NOT_FOUND);
|
||||||
|
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
||||||
|
|
||||||
|
}
|
||||||
|
UserSessionModel userSession = session.sessions().getUserSession(realm, state);
|
||||||
|
if (userSession == null) {
|
||||||
|
logger.error("no valid user session");
|
||||||
|
EventBuilder event = new EventBuilder(realm, session, clientConnection);
|
||||||
|
event.event(EventType.LOGOUT);
|
||||||
|
event.error(Errors.USER_SESSION_NOT_FOUND);
|
||||||
|
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
|
||||||
|
}
|
||||||
|
if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) {
|
||||||
|
logger.error("usersession in different state");
|
||||||
|
EventBuilder event = new EventBuilder(realm, session, clientConnection);
|
||||||
|
event.event(EventType.LOGOUT);
|
||||||
|
event.error(Errors.USER_SESSION_NOT_FOUND);
|
||||||
|
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.SESSION_NOT_ACTIVE);
|
||||||
|
}
|
||||||
|
return AuthenticationManager.finishBrowserLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BrokeredIdentityContext getFederatedIdentity(String response) {
|
public BrokeredIdentityContext getFederatedIdentity(String response) {
|
||||||
|
|
|
@ -141,6 +141,42 @@ public class IdentityProviderTest extends AbstractAdminTest {
|
||||||
assertEquals(ComponentRepresentation.SECRET_VALUE, rep.getConfig().get("clientSecret"));
|
assertEquals(ComponentRepresentation.SECRET_VALUE, rep.getConfig().get("clientSecret"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateWithBasicAuth() {
|
||||||
|
IdentityProviderRepresentation newIdentityProvider = createRep("new-identity-provider", "oidc");
|
||||||
|
|
||||||
|
newIdentityProvider.getConfig().put("clientId", "clientId");
|
||||||
|
newIdentityProvider.getConfig().put("clientSecret", "some secret value");
|
||||||
|
newIdentityProvider.getConfig().put("clientAuthMethod",OIDCLoginProtocol.CLIENT_SECRET_BASIC);
|
||||||
|
|
||||||
|
create(newIdentityProvider);
|
||||||
|
|
||||||
|
IdentityProviderResource identityProviderResource = realm.identityProviders().get("new-identity-provider");
|
||||||
|
|
||||||
|
assertNotNull(identityProviderResource);
|
||||||
|
|
||||||
|
IdentityProviderRepresentation representation = identityProviderResource.toRepresentation();
|
||||||
|
|
||||||
|
assertNotNull(representation);
|
||||||
|
|
||||||
|
assertNotNull(representation.getInternalId());
|
||||||
|
assertEquals("new-identity-provider", representation.getAlias());
|
||||||
|
assertEquals("oidc", representation.getProviderId());
|
||||||
|
assertEquals("clientId", representation.getConfig().get("clientId"));
|
||||||
|
assertEquals(ComponentRepresentation.SECRET_VALUE, representation.getConfig().get("clientSecret"));
|
||||||
|
assertEquals(OIDCLoginProtocol.CLIENT_SECRET_BASIC, representation.getConfig().get("clientAuthMethod"));
|
||||||
|
|
||||||
|
assertTrue(representation.isEnabled());
|
||||||
|
assertFalse(representation.isStoreToken());
|
||||||
|
assertFalse(representation.isTrustEmail());
|
||||||
|
|
||||||
|
assertEquals("some secret value", testingClient.testing("admin-client-test").getIdentityProviderConfig("new-identity-provider").get("clientSecret"));
|
||||||
|
|
||||||
|
IdentityProviderRepresentation rep = realm.identityProviders().findAll().stream().filter(i -> i.getAlias().equals("new-identity-provider")).findFirst().get();
|
||||||
|
assertEquals(ComponentRepresentation.SECRET_VALUE, rep.getConfig().get("clientSecret"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCreateWithJWT() {
|
public void testCreateWithJWT() {
|
||||||
IdentityProviderRepresentation newIdentityProvider = createRep("new-identity-provider", "oidc");
|
IdentityProviderRepresentation newIdentityProvider = createRep("new-identity-provider", "oidc");
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
package org.keycloak.testsuite.broker;
|
||||||
|
|
||||||
|
import org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
|
import org.keycloak.testsuite.arquillian.SuiteContext;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.keycloak.testsuite.broker.BrokerTestConstants.*;
|
||||||
|
import static org.keycloak.testsuite.broker.BrokerTestTools.createIdentityProvider;
|
||||||
|
|
||||||
|
public class KcOidcBrokerClientSecretBasicAuthTest extends KcOidcBrokerTest {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected BrokerConfiguration getBrokerConfiguration() {
|
||||||
|
return new KcOidcBrokerConfigurationWithBasicAuthAuthentication();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class KcOidcBrokerConfigurationWithBasicAuthAuthentication extends KcOidcBrokerConfiguration {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) {
|
||||||
|
IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID);
|
||||||
|
Map<String, String> config = idp.getConfig();
|
||||||
|
applyDefaultConfiguration(suiteContext, config);
|
||||||
|
config.put("clientAuthMethod", OIDCLoginProtocol.CLIENT_SECRET_BASIC);
|
||||||
|
return idp;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -174,7 +174,7 @@
|
||||||
ng-model="identityProvider.config.clientAuthMethod"
|
ng-model="identityProvider.config.clientAuthMethod"
|
||||||
required>
|
required>
|
||||||
<option id="clientAuth_post" name="clientAuth" value="client_secret_post" selected>{{:: 'client-auth.client_secret_post' | translate}}</option>
|
<option id="clientAuth_post" name="clientAuth" value="client_secret_post" selected>{{:: 'client-auth.client_secret_post' | translate}}</option>
|
||||||
<!-- <option id="clientAuth_basic" name="clientAuth" value="client_secret_basic">{{:: 'client-auth.client_secret_basic' | translate}}</option> -->
|
<option id="clientAuth_basic" name="clientAuth" value="client_secret_basic">{{:: 'client-auth.client_secret_basic' | translate}}</option>
|
||||||
<option id="clientAuth_secret_jwt" name="clientAuth" value="client_secret_jwt">{{:: 'client-auth.client_secret_jwt' | translate}}</option>
|
<option id="clientAuth_secret_jwt" name="clientAuth" value="client_secret_jwt">{{:: 'client-auth.client_secret_jwt' | translate}}</option>
|
||||||
<option id="clientAuth_privatekey_jwt" name="clientAuth" value="private_key_jwt">{{:: 'client-auth.private_key_jwt' | translate}}</option>
|
<option id="clientAuth_privatekey_jwt" name="clientAuth" value="private_key_jwt">{{:: 'client-auth.private_key_jwt' | translate}}</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -337,4 +337,4 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<kc-menu></kc-menu>
|
<kc-menu></kc-menu>
|
||||||
|
|
Loading…
Reference in a new issue