Skip to content

Add Microsoft Graph Delegated Authorization Realm Plugin #127910

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

Merged
merged 5 commits into from
May 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions docs/changelog/127910.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 127910
summary: Add Microsoft Graph Delegated Authorization Realm Plugin
area: Authorization
type: enhancement
issues: []
21 changes: 21 additions & 0 deletions plugins/microsoft-graph-authz/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

apply plugin: "elasticsearch.internal-java-rest-test"

esplugin {
name = "microsoft-graph-authz"
description = "Microsoft Graph Delegated Authorization Realm Plugin"
classname = "org.elasticsearch.xpack.security.authz.microsoft.MicrosoftGraphAuthzPlugin"
extendedPlugins = ["x-pack-security"]
}

dependencies {
compileOnly project(":x-pack:plugin:core")
}
19 changes: 19 additions & 0 deletions plugins/microsoft-graph-authz/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import org.elasticsearch.xpack.security.authz.microsoft.MicrosoftGraphAuthzPlugin;

module org.elasticsearch.plugin.security.authz {
requires org.elasticsearch.base;
requires org.elasticsearch.server;
requires org.elasticsearch.xcore;
requires org.elasticsearch.logging;

provides org.elasticsearch.xpack.core.security.SecurityExtension with MicrosoftGraphAuthzPlugin;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.xpack.security.authz.microsoft;

import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.xpack.core.security.SecurityExtension;
import org.elasticsearch.xpack.core.security.authc.Realm;

import java.util.List;
import java.util.Map;

public class MicrosoftGraphAuthzPlugin extends Plugin implements SecurityExtension {
@Override
public Map<String, Realm.Factory> getRealms(SecurityComponents components) {
return Map.of(MicrosoftGraphAuthzRealmSettings.REALM_TYPE, MicrosoftGraphAuthzRealm::new);
}

@Override
public List<Setting<?>> getSettings() {
return MicrosoftGraphAuthzRealmSettings.getSettings();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.xpack.security.authz.microsoft;

import org.elasticsearch.action.ActionListener;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.logging.LogManager;
import org.elasticsearch.logging.Logger;
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
import org.elasticsearch.xpack.core.security.authc.Realm;
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
import org.elasticsearch.xpack.core.security.user.User;

public class MicrosoftGraphAuthzRealm extends Realm {

private static final Logger logger = LogManager.getLogger(MicrosoftGraphAuthzRealm.class);

public MicrosoftGraphAuthzRealm(RealmConfig config) {
super(config);
}

@Override
public boolean supports(AuthenticationToken token) {
return false;
}

@Override
public AuthenticationToken token(ThreadContext context) {
return null;
}

@Override
public void authenticate(AuthenticationToken token, ActionListener<AuthenticationResult<User>> listener) {
listener.onResponse(AuthenticationResult.notHandled());
}

@Override
public void lookupUser(String username, ActionListener<User> listener) {
logger.info("Microsoft Graph Authz not yet implemented, returning empty roles for [{}]", username);
listener.onResponse(new User(username));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.xpack.security.authz.microsoft;

import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.xpack.core.security.authc.RealmSettings;

import java.util.ArrayList;
import java.util.List;

public class MicrosoftGraphAuthzRealmSettings {
public static final String REALM_TYPE = "microsoft_graph";

public static List<Setting<?>> getSettings() {
return new ArrayList<>(RealmSettings.getStandardSettings(REALM_TYPE));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.elasticsearch.xpack.security.authz.microsoft.MicrosoftGraphAuthzPlugin
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apply plugin: 'elasticsearch.internal-java-rest-test'

dependencies {
javaRestTestImplementation project(':x-pack:plugin:core')
javaRestTestImplementation project(':x-pack:plugin:security')
javaRestTestImplementation testArtifact(project(":x-pack:plugin:security:qa:saml-rest-tests"), "javaRestTest")
clusterPlugins project(':plugins:microsoft-graph-authz')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.xpack.security.authz.microsoft;

import org.elasticsearch.client.Request;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.core.PathUtils;
import org.elasticsearch.test.XContentTestUtils;
import org.elasticsearch.test.cluster.ElasticsearchCluster;
import org.elasticsearch.test.cluster.local.model.User;
import org.elasticsearch.test.cluster.util.resource.Resource;
import org.elasticsearch.test.rest.ESRestTestCase;
import org.elasticsearch.test.rest.ObjectPath;
import org.elasticsearch.xcontent.json.JsonXContent;
import org.elasticsearch.xpack.security.authc.saml.SamlIdpMetadataBuilder;
import org.elasticsearch.xpack.security.authc.saml.SamlResponseBuilder;
import org.junit.ClassRule;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;

import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;

public class MicrosoftGraphAuthzPluginIT extends ESRestTestCase {
public static ElasticsearchCluster cluster = initTestCluster();

@ClassRule
public static TestRule ruleChain = RuleChain.outerRule(cluster);

private static final String IDP_ENTITY_ID = "http://idp.example.org/";

private static ElasticsearchCluster initTestCluster() {
return ElasticsearchCluster.local()
.setting("xpack.security.enabled", "true")
.setting("xpack.license.self_generated.type", "trial")
.setting("xpack.security.authc.token.enabled", "true")
.setting("xpack.security.http.ssl.enabled", "false")
.plugin("microsoft-graph-authz")
.keystore("bootstrap.password", "x-pack-test-password")
.user("test_admin", "x-pack-test-password", User.ROOT_USER_ROLE, true)
.user("rest_test", "rest_password", User.ROOT_USER_ROLE, true)
.configFile("metadata.xml", Resource.fromString(getIDPMetadata()))
.setting("xpack.security.authc.realms.saml.saml1.order", "1")
.setting("xpack.security.authc.realms.saml.saml1.idp.entity_id", IDP_ENTITY_ID)
.setting("xpack.security.authc.realms.saml.saml1.idp.metadata.path", "metadata.xml")
.setting("xpack.security.authc.realms.saml.saml1.attributes.principal", "urn:oid:2.5.4.3")
.setting("xpack.security.authc.realms.saml.saml1.sp.entity_id", "http://sp/default.example.org/")
.setting("xpack.security.authc.realms.saml.saml1.sp.acs", "http://acs/default")
.setting("xpack.security.authc.realms.saml.saml1.sp.logout", "http://logout/default")
.setting("xpack.security.authc.realms.saml.saml1.authorization_realms", "microsoft_graph1")
.setting("xpack.security.authc.realms.microsoft_graph.microsoft_graph1.order", "2")
.build();
}

private static String getIDPMetadata() {
try {
var signingCert = PathUtils.get(MicrosoftGraphAuthzPluginIT.class.getResource("/saml/signing.crt").toURI());
return new SamlIdpMetadataBuilder().entityId(IDP_ENTITY_ID).idpUrl(IDP_ENTITY_ID).sign(signingCert).asString();
} catch (URISyntaxException | CertificateException | IOException exception) {
fail(exception);
}
return null;
}

@Override
protected String getTestRestCluster() {
return cluster.getHttpAddresses();
}

@Override
protected String getProtocol() {
return "http";
}

@Override
protected Settings restClientSettings() {
final String token = basicAuthHeaderValue("rest_test", new SecureString("rest_password".toCharArray()));
return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build();
}

@Override
protected boolean shouldConfigureProjects() {
return false;
}

public void testAuthenticationSuccessful() throws Exception {
final String username = randomAlphaOfLengthBetween(4, 12);
samlAuthWithMicrosoftGraphAuthz(username, getSamlAssertionJsonBodyString(username));
}

private String getSamlAssertionJsonBodyString(String username) throws Exception {
var message = new SamlResponseBuilder().spEntityId("http://sp/default.example.org/")
.idpEntityId(IDP_ENTITY_ID)
.acs(new URL("http://acs/default"))
.attribute("urn:oid:2.5.4.3", username)
.sign(getDataPath("/saml/signing.crt"), getDataPath("/saml/signing.key"), new char[0])
.asString();

final Map<String, Object> body = new HashMap<>();
body.put("content", Base64.getEncoder().encodeToString(message.getBytes(StandardCharsets.UTF_8)));
body.put("realm", "saml1");
return Strings.toString(JsonXContent.contentBuilder().map(body));
}

private void samlAuthWithMicrosoftGraphAuthz(String username, String samlAssertion) throws Exception {
var req = new Request("POST", "_security/saml/authenticate");
req.setJsonEntity(samlAssertion);
var resp = entityAsMap(client().performRequest(req));
List<String> roles = new XContentTestUtils.JsonMapView(entityAsMap(client().performRequest(req))).get("authentication.roles");
assertThat(resp.get("username"), equalTo(username));
// TODO add check for mapped groups and roles when available
assertThat(roles, empty());
assertThat(ObjectPath.evaluate(resp, "authentication.authentication_realm.name"), equalTo("saml1"));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#
# Script / Instructions to (re)generate the certificates + keys in this directory
#
# You can run this with bash, provided "elasticsearch-certutil" is somewhere on your path (or aliased)

#
# Step 1: Create a Signing Key in PEM format
#
elasticsearch-certutil cert --self-signed --pem --out ${PWD}/signing.zip -days 9999 -keysize 2048 -name "signing"
unzip signing.zip
mv signing/signing.* ./
rmdir signing
rm signing.zip

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDAjCCAeqgAwIBAgIVAIySNaWg1+64cquJnza2djHgwNuPMA0GCSqGSIb3DQEB
CwUAMBIxEDAOBgNVBAMTB3NpZ25pbmcwIBcNMjMwMjA4MDc0MjQ3WhgPMjA1MDA2
MjUwNzQyNDdaMBIxEDAOBgNVBAMTB3NpZ25pbmcwggEiMA0GCSqGSIb3DQEBAQUA
A4IBDwAwggEKAoIBAQC/EPQ4xX+8QyIUf+XAJM+U4jZQT3Pa+0pGZahXqx29VVGC
cbjTs4hGJ0OK0eDSN7Ess319D0ucIo9P631G/RQiEAjP7AuUOfBF5VvppEucncvU
dJlPTycW3kno9KYzRqIxtAcX44JsC3qm1M/D37dlscen+ZjdTEDeP1c87hSDJgi1
GAWUIBeT/7vHItpQcBv2xcJsaT26R2r62FTO/H0XNo8ElESKEcxN0pkfSCxik2g7
Yn4d+Lvx63otlC0dLPL1rdJQrCQKyAzhbGI6/35uAyes58D54hYImeiCm5p6IalL
ipST+CDTrYmpZm/EzypBH8hasCXDFTvgjBmQO1d1AgMBAAGjTTBLMB0GA1UdDgQW
BBTlR7Un5DDAwMJJ8D4pMJ4c7+JWgTAfBgNVHSMEGDAWgBTlR7Un5DDAwMJJ8D4p
MJ4c7+JWgTAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQAAeXo5MMlC/Rzw
TI6+U3rbr17n6s4M6a+PKJLY0tTfVscNwd40+WVQ8rvIIso6V1CREjvgMBuELK+6
2FTURf+EP58+TMrKEOSYWjee9fR/E29uXherZIBtHHSXOrv7P1g1FxYBxHwh4iOI
ZQIqrMAeJUQr00Tn1B2Fuqlyod1KA+1eTQw036lW5iBdpim5wFOE4HaGNYgP7J8j
FBIqIFVDFRH27h9sDfs/Uqk3OGF2sWnovwB/x76yGjDAVejY5wG0ZultYexMRnPo
KshiiSzcortBA7CEzQtYbxP6X0DUsWD3U8Z4EsiTIWHLxRGZzWurZIdOrbi8OMFQ
0a27kQsa
-----END CERTIFICATE-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAvxD0OMV/vEMiFH/lwCTPlOI2UE9z2vtKRmWoV6sdvVVRgnG4
07OIRidDitHg0jexLLN9fQ9LnCKPT+t9Rv0UIhAIz+wLlDnwReVb6aRLnJ3L1HSZ
T08nFt5J6PSmM0aiMbQHF+OCbAt6ptTPw9+3ZbHHp/mY3UxA3j9XPO4UgyYItRgF
lCAXk/+7xyLaUHAb9sXCbGk9ukdq+thUzvx9FzaPBJREihHMTdKZH0gsYpNoO2J+
Hfi78et6LZQtHSzy9a3SUKwkCsgM4WxiOv9+bgMnrOfA+eIWCJnogpuaeiGpS4qU
k/gg062JqWZvxM8qQR/IWrAlwxU74IwZkDtXdQIDAQABAoIBAADRqN5VGGOn8lgz
USU2SIPV8WTS5lymBEQ80NB+uFv/UYq1LpU3uTSQcVrBugyPS7g06muJZSn9lZmC
g/u0f4GC30Ahk04hbnJ6QHSGAq317jGnsKA1UbtTHMQock2Yy/6vC8hng6paD+lG
/b9URj65GPGTIWYyGrq+e5hUWUGo+5SNY1B8iq1NYsP9WQ3D8s87wKBTcXDyolid
BgpVExBD0LM+K3X8pOP/8Hh7qSWZBW7DpvcykcZ5OHmQjAkYizkjVV/mR9epTQHT
gS52PniKFNT6t4W2/HwtNCQbdHm3T95s8kyyQrkpdnL5Dxn/UGgDy62o7CIuAZS7
xygf5eECgYEA/AG4zGycGqdhccjEZnaQ23qcutakWLK82RSZLwjstqlwtRLVW1Cp
Mw2k9lmcLcgXyTlrnD1weRvmG7HFHoAcy+CWnv0boFn5W3TDu9GkFHoI9/+n6hUG
nUQi+OL9G+KslOt+d4nMBCSYm1t2kcC9qpEOTZz+X1F8K4moZZEEGJUCgYEAwhgG
HPFFt/WZI5rIH8GRki9AKvp7EEOK6Nrl7XrtKiKMjA0NLF2wZj0RViRAax3ca1W6
EjyxmSSMcNRWS1APyTRTtvL6FLdviUmQHSOiZ6TzPauvn9KMKIAYyE5ZijTtN9nz
TZacH6PmlMBzLceLK2V2FAhA4tQx7M7asKsZK2ECgYEAsY4JBUc0yXbLKl85ObQq
JemCygV3L+NnOU/RChmwppZFid7mInt3azge1U+XwY3sbGOflSqYt0vX2gVrjCzZ
nS/1D7nnoBgkn7JqQkfX4nGFJi6jwULlMSMTvOY5TU9tJ1Ow/EpDS1v5heRwawsw
1x9yw25srv37jbVkx4LgLu0CgYBDq16OPqxA+9adbDxznegj4Gdt1JCNVg8bKh5Z
0q7XLt5zgaVjH3L94jKmJtNyxSFxJp1N+G0u6GgyekVv0oT+cEjzkvkPufigE86z
6hWYLxFDIhWEEkMdZ7O8OlzLa7J883b5SRY7jcg5enNttZFW2vP0/f+pVbPmTSQ/
zhdjYQKBgQDPwQ5gq6et0D6R35mFsOUIl2p2Py4Rp6UHLGbyooeVy0HCwz7fB/da
fBx7HYDeYqOS3yQnyWkzzn+GXU3lzboBCQ9B17NlxVIbIyqAHhtPTxNWEwqf1nsa
Q/a61ry0tRnMTIM6bkjXVoHY5popvkN9dQA1VCD58VYerSdh/3lRTQ==
-----END RSA PRIVATE KEY-----
Loading