Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support 3rd party initiated login for OpenID Connect #38474

Merged
merged 32 commits into from
Feb 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6448271
Implement code and implicit flows
jkakavas Jan 23, 2019
92f63ef
fix licenses and SHAs for introduced dependencies
jkakavas Jan 23, 2019
19034ce
Support userinfo requests
jkakavas Jan 23, 2019
aaa5426
Merge remote-tracking branch 'origin/master' into oidc-realm-authenti…
jkakavas Jan 24, 2019
6614057
Add few tests
jkakavas Jan 24, 2019
b70526a
Fix bugs and add unit tests
jkakavas Jan 25, 2019
422319d
Merge remote-tracking branch 'origin/feature-oidc-realm' into oidc-re…
jkakavas Jan 25, 2019
05910d9
Address Feedback
jkakavas Jan 29, 2019
22e8bb7
Merge remote-tracking branch 'origin/feature-oidc-realm' into oidc-re…
jkakavas Jan 29, 2019
b8aa5be
Fix Access Token validation
jkakavas Jan 30, 2019
e3204f5
Completes security tests
jkakavas Jan 30, 2019
ad493a9
Merge remote-tracking branch 'origin/feature-oidc-realm' into oidc-re…
jkakavas Jan 30, 2019
b3cbd8d
Fix checkstyle
jkakavas Jan 30, 2019
0a0ae0e
Make JWKSource reloading async
jkakavas Feb 1, 2019
bc1f552
Merge remote-tracking branch 'origin/feature-oidc-realm' into oidc-re…
jkakavas Feb 1, 2019
baadcdb
fix thirdPartyAudit
jkakavas Feb 1, 2019
09413b5
address feedback
jkakavas Feb 1, 2019
59bc122
cleanup
jkakavas Feb 2, 2019
b368ffc
re-introduce the option for facilitators to pass state and nonce values
jkakavas Feb 3, 2019
81a1770
remove unused import
jkakavas Feb 3, 2019
c9c1cf7
Merge branch 'feature-oidc-realm' into oidc-realm-authentication-flows
jkakavas Feb 3, 2019
3450541
fix tests
jkakavas Feb 3, 2019
8fd7f31
Support 3rd party initiated authentication
jkakavas Feb 4, 2019
354f7fd
Merge branch 'oidc-realm-authentication-flows' into 3rd-party-initiated
jkakavas Feb 4, 2019
5937ec8
Merge branch 'feature-oidc-realm' into 3rd-party-initiated
jkakavas Feb 5, 2019
d6a8474
Merge branch 'feature-oidc-realm' into 3rd-party-initiated
jkakavas Feb 6, 2019
afaf2e9
Merge branch 'feature-oidc-realm' into 3rd-party-initiated
jkakavas Feb 11, 2019
4e0788e
address feedback
jkakavas Feb 11, 2019
0fbe3c7
add parser field
jkakavas Feb 13, 2019
82fa677
Merge branch 'feature-oidc-realm' into 3rd-party-initiated
jkakavas Feb 13, 2019
af22291
Merge remote-tracking branch 'origin/feature-oidc-realm' into 3rd-par…
jkakavas Feb 22, 2019
00baca1
address feedback
jkakavas Feb 22, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,17 @@
*/
public class OpenIdConnectPrepareAuthenticationRequest extends ActionRequest {

/**
* The name of the OpenID Connect realm in the configuration that should be used for authentication
*/
private String realmName;
/**
* In case of a
* <a href="https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin">3rd party initiated authentication</a>, the
* issuer that the User Agent needs to be redirected to for authentication
*/
private String issuer;
private String loginHint;
private String state;
private String nonce;

Expand All @@ -36,10 +46,22 @@ public String getNonce() {
return nonce;
}

public String getIssuer() {
return issuer;
}

public String getLoginHint() {
return loginHint;
}

public void setRealmName(String realmName) {
this.realmName = realmName;
}

public void setIssuer(String issuer) {
this.issuer = issuer;
}

public void setState(String state) {
this.state = state;
}
Expand All @@ -48,29 +70,40 @@ public void setNonce(String nonce) {
this.nonce = nonce;
}

public void setLoginHint(String loginHint) {
this.loginHint = loginHint;
}

public OpenIdConnectPrepareAuthenticationRequest() {
}

public OpenIdConnectPrepareAuthenticationRequest(StreamInput in) throws IOException {
super.readFrom(in);
realmName = in.readString();
realmName = in.readOptionalString();
issuer = in.readOptionalString();
loginHint = in.readOptionalString();
state = in.readOptionalString();
nonce = in.readOptionalString();
}

@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (Strings.hasText(realmName) == false) {
validationException = addValidationError("realm name must be provided", null);
if (Strings.hasText(realmName) == false && Strings.hasText(issuer) == false) {
validationException = addValidationError("one of [realm, issuer] must be provided", null);
}
if (Strings.hasText(realmName) && Strings.hasText(issuer)) {
validationException = addValidationError("only one of [realm, issuer] can be provided in the same request", null);
}
return validationException;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(realmName);
out.writeOptionalString(realmName);
out.writeOptionalString(issuer);
out.writeOptionalString(loginHint);
out.writeOptionalString(state);
out.writeOptionalString(nonce);
}
Expand All @@ -81,7 +114,8 @@ public void readFrom(StreamInput in) {
}

public String toString() {
return "{realmName=" + realmName + ", state=" + state + ", nonce=" + nonce + "}";
return "{realmName=" + realmName + ", issuer=" + issuer + ", login_hint=" +
loginHint + ", state=" + state + ", nonce=" + nonce + "}";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.HandledTransportAction;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.tasks.Task;
Expand All @@ -21,6 +22,8 @@
import org.elasticsearch.xpack.security.authc.Realms;
import org.elasticsearch.xpack.security.authc.oidc.OpenIdConnectRealm;

import java.util.List;
import java.util.stream.Collectors;

public class TransportOpenIdConnectPrepareAuthenticationAction extends HandledTransportAction<OpenIdConnectPrepareAuthenticationRequest,
OpenIdConnectPrepareAuthenticationResponse> {
Expand All @@ -38,19 +41,40 @@ public TransportOpenIdConnectPrepareAuthenticationAction(TransportService transp
@Override
protected void doExecute(Task task, OpenIdConnectPrepareAuthenticationRequest request,
ActionListener<OpenIdConnectPrepareAuthenticationResponse> listener) {
final Realm realm = this.realms.realm(request.getRealmName());
if (null == realm || realm instanceof OpenIdConnectRealm == false) {
Realm realm = null;
if (Strings.hasText(request.getIssuer())) {
List<OpenIdConnectRealm> matchingRealms = this.realms.stream()
.filter(r -> r instanceof OpenIdConnectRealm && ((OpenIdConnectRealm) r).isIssuerValid(request.getIssuer()))
.map(r -> (OpenIdConnectRealm) r)
.filter(r -> r.isIssuerValid(request.getIssuer()))
.collect(Collectors.toList());
if (matchingRealms.isEmpty()) {
listener.onFailure(
new ElasticsearchSecurityException("Cannot find OpenID Connect realm with issuer [{}]", request.getIssuer()));
} else if (matchingRealms.size() > 1) {
listener.onFailure(
new ElasticsearchSecurityException("Found multiple OpenID Connect realm with issuer [{}]", request.getIssuer()));
} else {
realm = matchingRealms.get(0);
}
} else if (Strings.hasText(request.getRealmName())) {
realm = this.realms.realm(request.getRealmName());
}

if (realm instanceof OpenIdConnectRealm) {
prepareAuthenticationResponse((OpenIdConnectRealm) realm, request.getState(), request.getNonce(), request.getLoginHint(),
listener);
} else {
listener.onFailure(
new ElasticsearchSecurityException("Cannot find OpenID Connect realm with name [{}]", request.getRealmName()));
} else {
prepareAuthenticationResponse((OpenIdConnectRealm) realm, request.getState(), request.getNonce(), listener);
}
}

private void prepareAuthenticationResponse(OpenIdConnectRealm realm, String state, String nonce,
private void prepareAuthenticationResponse(OpenIdConnectRealm realm, String state, String nonce, String loginHint,
ActionListener<OpenIdConnectPrepareAuthenticationResponse> listener) {
try {
final OpenIdConnectPrepareAuthenticationResponse authenticationResponse = realm.buildAuthenticationRequestUri(state, nonce);
final OpenIdConnectPrepareAuthenticationResponse authenticationResponse =
realm.buildAuthenticationRequestUri(state, nonce, loginHint);
listener.onResponse(authenticationResponse);
} catch (ElasticsearchException e) {
listener.onFailure(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,26 +294,33 @@ private static String require(RealmConfig config, Setting.AffixSetting<String> s
*
* @param existingState An existing state that can be reused or null if we need to generate one
* @param existingNonce An existing nonce that can be reused or null if we need to generate one
* @param loginHint A String with a login hint to add to the authentication request in case of a 3rd party initiated login
*
* @return an {@link OpenIdConnectPrepareAuthenticationResponse}
*/
public OpenIdConnectPrepareAuthenticationResponse buildAuthenticationRequestUri(@Nullable String existingState,
@Nullable String existingNonce) {
@Nullable String existingNonce,
@Nullable String loginHint) {
final State state = existingState != null ? new State(existingState) : new State();
final Nonce nonce = existingNonce != null ? new Nonce(existingNonce) : new Nonce();
final AuthenticationRequest authenticationRequest = new AuthenticationRequest(
opConfiguration.getAuthorizationEndpoint(),
rpConfiguration.getResponseType(),
final AuthenticationRequest.Builder builder = new AuthenticationRequest.Builder(rpConfiguration.getResponseType(),
rpConfiguration.getRequestedScope(),
rpConfiguration.getClientId(),
rpConfiguration.getRedirectUri(),
state,
nonce);

return new OpenIdConnectPrepareAuthenticationResponse(authenticationRequest.toURI().toString(),
rpConfiguration.getRedirectUri())
.endpointURI(opConfiguration.getAuthorizationEndpoint())
.state(state)
.nonce(nonce);
if (Strings.hasText(loginHint)) {
builder.loginHint(loginHint);
}
return new OpenIdConnectPrepareAuthenticationResponse(builder.build().toURI().toString(),
state.getValue(), nonce.getValue());
}

public boolean isIssuerValid(String issuer) {
return this.opConfiguration.getIssuer().getValue().equals(issuer);
}

@Override
public void close() {
openIdConnectAuthenticator.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public class RestOpenIdConnectPrepareAuthenticationAction extends OpenIdConnectB

static {
PARSER.declareString(OpenIdConnectPrepareAuthenticationRequest::setRealmName, new ParseField("realm"));
PARSER.declareString(OpenIdConnectPrepareAuthenticationRequest::setIssuer, new ParseField("iss"));
Copy link
Contributor

@albertzaharovits albertzaharovits Feb 13, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add:

PARSER.declareString(OpenIdConnectPrepareAuthenticationRequest::setLoginHint, new ParseField("login_hint"));

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦‍♂️

PARSER.declareString(OpenIdConnectPrepareAuthenticationRequest::setLoginHint, new ParseField("login_hint"));
PARSER.declareString(OpenIdConnectPrepareAuthenticationRequest::setState, new ParseField("state"));
PARSER.declareString(OpenIdConnectPrepareAuthenticationRequest::setNonce, new ParseField("nonce"));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ public void testSerialization() throws IOException {
final OpenIdConnectPrepareAuthenticationRequest deserialized =
new OpenIdConnectPrepareAuthenticationRequest(out.bytes().streamInput());
assertThat(deserialized.getRealmName(), equalTo("oidc-realm1"));

final OpenIdConnectPrepareAuthenticationRequest request2 = new OpenIdConnectPrepareAuthenticationRequest();
request2.setIssuer("https://op.company.org/");
final BytesStreamOutput out2 = new BytesStreamOutput();
request2.writeTo(out2);

final OpenIdConnectPrepareAuthenticationRequest deserialized2 =
new OpenIdConnectPrepareAuthenticationRequest(out2.bytes().streamInput());
assertThat(deserialized2.getIssuer(), equalTo("https://op.company.org/"));
}

public void testSerializationWithStateAndNonce() throws IOException {
Expand All @@ -50,6 +59,15 @@ public void testValidation() {
final ActionRequestValidationException validation = request.validate();
assertNotNull(validation);
assertThat(validation.validationErrors().size(), equalTo(1));
assertThat(validation.validationErrors().get(0), containsString("realm name must be provided"));
assertThat(validation.validationErrors().get(0), containsString("one of [realm, issuer] must be provided"));

final OpenIdConnectPrepareAuthenticationRequest request2 = new OpenIdConnectPrepareAuthenticationRequest();
request2.setRealmName("oidc-realm1");
request2.setIssuer("https://op.company.org/");
final ActionRequestValidationException validation2 = request2.validate();
assertNotNull(validation2);
assertThat(validation2.validationErrors().size(), equalTo(1));
assertThat(validation2.validationErrors().get(0),
containsString("only one of [realm, issuer] can be provided in the same request"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,15 +162,15 @@ public void testBuildRelyingPartyConfigWithoutOpenIdScope() {
Arrays.asList("scope1", "scope2"));
final OpenIdConnectRealm realm = new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null,
null);
final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(null, null);
final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(null, null, null);
final String state = response.getState();
final String nonce = response.getNonce();
assertThat(response.getAuthenticationRequestUrl(),
equalTo("https://op.example.com/login?scope=scope1+scope2+openid&response_type=code" +
"&redirect_uri=https%3A%2F%2Frp.my.com%2Fcb&state=" + state + "&nonce=" + nonce + "&client_id=rp-my"));
}

public void testBuilidingAuthenticationRequest() {
public void testBuildingAuthenticationRequest() {
final Settings.Builder settingsBuilder = Settings.builder()
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login")
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.com/token")
Expand All @@ -185,7 +185,7 @@ public void testBuilidingAuthenticationRequest() {
Arrays.asList("openid", "scope1", "scope2"));
final OpenIdConnectRealm realm = new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null,
null);
final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(null, null);
final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(null, null, null);
final String state = response.getState();
final String nonce = response.getNonce();
assertThat(response.getAuthenticationRequestUrl(),
Expand All @@ -194,7 +194,7 @@ public void testBuilidingAuthenticationRequest() {
}


public void testBuilidingAuthenticationRequestWithDefaultScope() {
public void testBuildingAuthenticationRequestWithDefaultScope() {
final Settings.Builder settingsBuilder = Settings.builder()
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login")
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.com/token")
Expand All @@ -207,14 +207,14 @@ public void testBuilidingAuthenticationRequestWithDefaultScope() {
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code");
final OpenIdConnectRealm realm = new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null,
null);
final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(null, null);
final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(null, null, null);
final String state = response.getState();
final String nonce = response.getNonce();
assertThat(response.getAuthenticationRequestUrl(), equalTo("https://op.example.com/login?scope=openid&response_type=code" +
"&redirect_uri=https%3A%2F%2Frp.my.com%2Fcb&state=" + state + "&nonce=" + nonce + "&client_id=rp-my"));
}

public void testBuilidingAuthenticationRequestWithExistingStateAndNonce() {
public void testBuildingAuthenticationRequestWithExistingStateAndNonce() {
final Settings.Builder settingsBuilder = Settings.builder()
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login")
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.com/token")
Expand All @@ -229,12 +229,35 @@ public void testBuilidingAuthenticationRequestWithExistingStateAndNonce() {
null);
final String state = new State().getValue();
final String nonce = new Nonce().getValue();
final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(state, nonce);
final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(state, nonce, null);

assertThat(response.getAuthenticationRequestUrl(), equalTo("https://op.example.com/login?scope=openid&response_type=code" +
"&redirect_uri=https%3A%2F%2Frp.my.com%2Fcb&state=" + state + "&nonce=" + nonce + "&client_id=rp-my"));
}

public void testBuildingAuthenticationRequestWithLoginHint() {
final Settings.Builder settingsBuilder = Settings.builder()
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_AUTHORIZATION_ENDPOINT), "https://op.example.com/login")
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_TOKEN_ENDPOINT), "https://op.example.com/token")
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_ISSUER), "https://op.example.com")
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_NAME), "the op")
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.OP_JWKSET_PATH), "https://op.example.com/jwks.json")
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.PRINCIPAL_CLAIM.getClaim()), "sub")
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_REDIRECT_URI), "https://rp.my.com/cb")
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_CLIENT_ID), "rp-my")
.put(getFullSettingKey(REALM_NAME, OpenIdConnectRealmSettings.RP_RESPONSE_TYPE), "code");
final OpenIdConnectRealm realm = new OpenIdConnectRealm(buildConfig(settingsBuilder.build()), null,
null);
final String state = new State().getValue();
final String nonce = new Nonce().getValue();
final String thehint = randomAlphaOfLength(8);
final OpenIdConnectPrepareAuthenticationResponse response = realm.buildAuthenticationRequestUri(state, nonce, thehint);

assertThat(response.getAuthenticationRequestUrl(), equalTo("https://op.example.com/login?login_hint=" + thehint +
"&scope=openid&response_type=code&redirect_uri=https%3A%2F%2Frp.my.com%2Fcb&state=" +
state + "&nonce=" + nonce + "&client_id=rp-my"));
}

private AuthenticationResult authenticateWithOidc(UserRoleMapper roleMapper, boolean notPopulateMetadata, boolean useAuthorizingRealm)
throws Exception {

Expand Down