Skip to content
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
10 changes: 9 additions & 1 deletion src/client/test/setup-keycloak.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# This script requires keycloak to be already running and configures it to be compatible with the unit tests by setting up a realm, a user etc...
# To start a keycloak instance in the first place, the following is sufficient:
# docker run -p 8090:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:25.0.4 start-dev
# docker run -p 127.0.0.1:8090:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:25.0.4 start-dev
#
# For more information see: https://www.keycloak.org/getting-started/getting-started-docker

Expand All @@ -15,6 +15,8 @@ while ! curl -s "$URL" >/dev/null; do
sleep 1
done

# For more information about admin endpoints refer to https://www.keycloak.org/docs-api/latest/rest-api/index.html

echo 'Getting access token'
AUTH="Authorization: Bearer $(curl -s -X POST "$URL/realms/master/protocol/openid-connect/token" -H 'Accept: application/json' -H 'Content-Type: application/x-www-form-urlencoded' -d 'grant_type=password&username=admin&password='"$ADMIN_PASSWORD"'&client_id=admin-cli' | jq -r '.access_token')"

Expand All @@ -27,5 +29,11 @@ curl -s -X POST "$URL/admin/realms/testrealm/users" -H 'Content-Type: applicatio
echo 'Adding a client with client id "testclientid"'
curl -s -X POST "$URL/admin/realms/testrealm/clients" -H 'Content-Type: application/json' -H "$AUTH" -d '{"clientId":"testclientid","enabled":true,"redirectUris":["'"$REDIRECT_URI"'"],"publicClient":true}'

echo 'Updating client to return role names in userinfo for openid scope'
ROLES="$(curl -s -X GET "$URL/admin/realms/testrealm/client-scopes" -H 'Content-Type: application/json' -H "$AUTH" | jq '.[] | select(.name=="roles")')"
CLIENTSCOPEID="$(echo "$ROLES" | jq -r '.id')"
MAPPERID="$(echo "$ROLES" | jq -r '.protocolMappers[] | select(.name == "client roles") | .id')"
curl -s -X PUT "$URL/admin/realms/testrealm/client-scopes/$CLIENTSCOPEID/protocol-mappers/models/$MAPPERID" -H 'Content-Type: application/json' -H "$AUTH" -d '{"id":"'"$MAPPERID"'","protocol":"openid-connect","protocolMapper":"oidc-usermodel-client-role-mapper","name":"client roles","consentRequired":false,"config":{"introspection.token.claim":true,"userinfo.token.claim":true,"claim.name":"resource_access.${client_id}.roles", "jsonType.label":"String","multivalued":true,"userinfo.token.claim":true}}' # it is intended that ${client_id} should not shell expand here

echo 'Adding a confidential client with client id "confidentialclientid" and secret "secret00000000000000000000000000"'
curl -s -X POST "$URL/admin/realms/testrealm/clients" -H 'Content-Type: application/json' -H "$AUTH" -d '{"clientId":"confidentialclientid","enabled":true,"redirectUris":["'"$REDIRECT_URI"'"],"publicClient":false,"clientAuthenticatorType":"client-secret","directAccessGrantsEnabled":true,"secret":"secret00000000000000000000000000"}'
11 changes: 5 additions & 6 deletions src/majordomo/include/majordomo/Cryptography.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,27 @@ class KeyHash {
unsigned char hash[crypto_generichash_KEYBYTES];
};

std::pair<PublicKey, PrivateKey> generateKeyPair() {
inline std::pair<PublicKey, PrivateKey> generateKeyPair() {
std::pair<PublicKey, PrivateKey> result;
crypto_sign_keypair(result.first.key, result.second.key);
return result;
}

KeyHash publicKeyHash(const PublicKey &key) {
inline KeyHash publicKeyHash(const PublicKey &key) {
KeyHash result;
crypto_generichash(result.hash, sizeof result.hash, key.key, crypto_sign_PUBLICKEYBYTES, nullptr, 0);
return result;
}

std::string messageHash(const mdp::Message &msg) {
inline std::string messageHash(const mdp::Message &msg) {
unsigned char buf[crypto_generichash_KEYBYTES];
for (size_t i = 0; i < crypto_generichash_KEYBYTES; ++i) {
buf[i] = msg.rbac[i];
}
return std::string(buf, buf + crypto_generichash_KEYBYTES);
}

void sign(mdp::Message &msg, const PrivateKey &key, const KeyHash &hash) {
inline void sign(mdp::Message &msg, const PrivateKey &key, const KeyHash &hash) {
// write key hash followed by signature
unsigned char buf[crypto_generichash_KEYBYTES + crypto_sign_BYTES];
std::copy(hash.hash, hash.hash + crypto_generichash_KEYBYTES, buf);
Expand All @@ -53,7 +53,7 @@ void sign(mdp::Message &msg, const PrivateKey &key, const KeyHash &hash) {
msg.rbac = rbac;
}

bool verify(mdp::Message &msg, const PublicKey &key) {
inline bool verify(const mdp::Message &msg, const PublicKey &key) {
auto data = msg.data;
unsigned char sig[crypto_sign_BYTES];
for (size_t i = 0; i < crypto_sign_BYTES; ++i) {
Expand All @@ -62,7 +62,6 @@ bool verify(mdp::Message &msg, const PublicKey &key) {
}
return !crypto_sign_verify_detached(sig, data.data(), data.size(), key.key);
}

}

} // namespace opencmw::majordomo::cryptography
Expand Down
60 changes: 50 additions & 10 deletions src/services/include/services/OAuthClient.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@
std::string secret;
std::string accessToken;
std::string refreshToken;
std::string roles;
};
} // namespace opencmw

ENABLE_REFLECTION_FOR(opencmw::OAuthContext, secretToken)
ENABLE_REFLECTION_FOR(opencmw::OAuthInput, scope, clientId, clientSecret, secret, publicKey)
ENABLE_REFLECTION_FOR(opencmw::OAuthOutput, authorizationUri, secret, accessToken, refreshToken)
ENABLE_REFLECTION_FOR(opencmw::OAuthOutput, authorizationUri, secret, accessToken, refreshToken, roles)

namespace opencmw {

Expand Down Expand Up @@ -91,26 +92,33 @@
StrictUri _redirectUri;
StrictUri _endpoint;
StrictUri _tokenEndpoint;
StrictUri _userinfoEndpoint;
httplib::Server _srv;
std::unique_ptr<std::thread> _thread;
std::function<void(const std::string &code, const std::string &state)> _endpointCallback;

public:
explicit OAuthClient(StrictUri redirectUri, StrictUri endpoint, StrictUri tokenEndpoint)
: _redirectUri(redirectUri), _endpoint(endpoint), _tokenEndpoint(tokenEndpoint) {
explicit OAuthClient(StrictUri redirectUri, StrictUri endpoint, StrictUri tokenEndpoint, StrictUri userinfoEndpoint = StrictUri(""))

Check warning on line 101 in src/services/include/services/OAuthClient.hpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Pass large object "redirectUri" by reference to const.

See more on https://sonarcloud.io/project/issues?id=fair-acc_opencmw-cpp&issues=AZ1OGiSkPU4l3pg_iGc3&open=AZ1OGiSkPU4l3pg_iGc3&pullRequest=402

Check warning on line 101 in src/services/include/services/OAuthClient.hpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Pass large object "endpoint" by reference to const.

See more on https://sonarcloud.io/project/issues?id=fair-acc_opencmw-cpp&issues=AZ1OGiSkPU4l3pg_iGc4&open=AZ1OGiSkPU4l3pg_iGc4&pullRequest=402

Check warning on line 101 in src/services/include/services/OAuthClient.hpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Pass large object "userinfoEndpoint" by reference to const.

See more on https://sonarcloud.io/project/issues?id=fair-acc_opencmw-cpp&issues=AZ1OGiSkPU4l3pg_iGc6&open=AZ1OGiSkPU4l3pg_iGc6&pullRequest=402

Check warning on line 101 in src/services/include/services/OAuthClient.hpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Pass large object "tokenEndpoint" by reference to const.

See more on https://sonarcloud.io/project/issues?id=fair-acc_opencmw-cpp&issues=AZ1OGiSkPU4l3pg_iGc5&open=AZ1OGiSkPU4l3pg_iGc5&pullRequest=402
: _redirectUri(redirectUri), _endpoint(endpoint), _tokenEndpoint(tokenEndpoint), _userinfoEndpoint(userinfoEndpoint) {
_srv.Get("/", [&](const httplib::Request req, httplib::Response &res) {

Check warning on line 103 in src/services/include/services/OAuthClient.hpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This lambda has 25 lines, which is greater than the 20 lines authorized. Split it into several lambdas or functions, or make it a named function.

See more on https://sonarcloud.io/project/issues?id=fair-acc_opencmw-cpp&issues=AZ1OGiSkPU4l3pg_iGc7&open=AZ1OGiSkPU4l3pg_iGc7&pullRequest=402
std::string code, state;
std::string code, state, error;

Check warning on line 104 in src/services/include/services/OAuthClient.hpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define each identifier in a dedicated statement.

See more on https://sonarcloud.io/project/issues?id=fair-acc_opencmw-cpp&issues=AZ1OGiSkPU4l3pg_iGc8&open=AZ1OGiSkPU4l3pg_iGc8&pullRequest=402
// response as per https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
for (const auto &[k, v] : req.params) {
if (k == "code") {
code = v;
} else if (k == "state") {
state = v;
} else if (k == "error_description") {
error = v;
}
}
if (code.empty()) {
res.set_content("Did not receive an RFC 6749-compliant response.", "text/plain");
res.status = 401; // Unauthorized
if (error.empty()) {
res.set_content("Did not receive an RFC 6749-compliant response.", "text/plain");
} else {
res.set_content("Error: " + error, "text/plain");
}
} else {
res.set_content("Authorization complete. You can close this browser window now.\n", "text/plain");
if (_endpointCallback) {
Expand All @@ -119,7 +127,9 @@
}
});
_thread = std::make_unique<std::thread>([this]() {
_srv.listen(*_redirectUri.hostName(), *_redirectUri.port());
if (!_srv.listen(*_redirectUri.hostName(), *_redirectUri.port())) {
std::println("Failed to listen on port {}", *_redirectUri.port());
};

Check warning on line 132 in src/services/include/services/OAuthClient.hpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this empty statement.

See more on https://sonarcloud.io/project/issues?id=fair-acc_opencmw-cpp&issues=AZ1OGiSkPU4l3pg_iGc9&open=AZ1OGiSkPU4l3pg_iGc9&pullRequest=402
});
}

Expand Down Expand Up @@ -205,6 +215,33 @@
return getAccessToken({ { "grant_type", "refresh_token" }, { "refresh_token", refreshToken }, { "client_id", clientId } });
}

std::string getAssignedRoles(const std::string &accessToken) {

Check warning on line 218 in src/services/include/services/OAuthClient.hpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This function should be declared "const".

See more on https://sonarcloud.io/project/issues?id=fair-acc_opencmw-cpp&issues=AZ1OGiSkPU4l3pg_iGc-&open=AZ1OGiSkPU4l3pg_iGc-&pullRequest=402
auto client = getClient(_userinfoEndpoint);
if (!client) {
return {};
}

httplib::Headers headers{ { { "Authorization", "Bearer " + accessToken } } };
auto res = client->Get(*_userinfoEndpoint.path(), headers);
if (!res || res->status != 200) {
return {};
}
std::string s{ res->body.c_str() };
const std::string marker{ "\"account\":{\"roles\":[" };

Check warning on line 230 in src/services/include/services/OAuthClient.hpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Convert this string literal to a raw string literal.

See more on https://sonarcloud.io/project/issues?id=fair-acc_opencmw-cpp&issues=AZ1OGiSkPU4l3pg_iGc_&open=AZ1OGiSkPU4l3pg_iGc_&pullRequest=402
auto start = s.find(marker);
if (start == std::string::npos) {
return {};
}
start += marker.size();
const auto end = s.find("]", start);
if (end == std::string::npos) {
return {};
}
std::string roles = s.substr(start, end - start);
std::erase(roles, '\"');
return roles;
}

void stop() {
_srv.stop();
_thread->join();
Expand Down Expand Up @@ -238,11 +275,11 @@

class OAuthWorker : public OAuthWorkerType {
public:
explicit OAuthWorker(StrictUri redirectUri, StrictUri endpoint, StrictUri tokenEndpoint, StrictUri brokerAddress, const zmq::Context &context, majordomo::Settings settings = {})
: OAuthWorkerType(brokerAddress, {}, context, settings), _client(redirectUri, endpoint, tokenEndpoint), _keystore(brokerAddress, context, settings) { init(); };
explicit OAuthWorker(StrictUri redirectUri, StrictUri endpoint, StrictUri tokenEndpoint, StrictUri userinfoEndpoint, StrictUri brokerAddress, const zmq::Context &context, majordomo::Settings settings = {})

Check warning on line 278 in src/services/include/services/OAuthClient.hpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Pass large object "settings" by reference to const.

See more on https://sonarcloud.io/project/issues?id=fair-acc_opencmw-cpp&issues=AZ1OGiSkPU4l3pg_iGdF&open=AZ1OGiSkPU4l3pg_iGdF&pullRequest=402

Check warning on line 278 in src/services/include/services/OAuthClient.hpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Pass large object "endpoint" by reference to const.

See more on https://sonarcloud.io/project/issues?id=fair-acc_opencmw-cpp&issues=AZ1OGiSkPU4l3pg_iGdB&open=AZ1OGiSkPU4l3pg_iGdB&pullRequest=402

Check warning on line 278 in src/services/include/services/OAuthClient.hpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Pass large object "redirectUri" by reference to const.

See more on https://sonarcloud.io/project/issues?id=fair-acc_opencmw-cpp&issues=AZ1OGiSkPU4l3pg_iGdA&open=AZ1OGiSkPU4l3pg_iGdA&pullRequest=402

Check warning on line 278 in src/services/include/services/OAuthClient.hpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Pass large object "tokenEndpoint" by reference to const.

See more on https://sonarcloud.io/project/issues?id=fair-acc_opencmw-cpp&issues=AZ1OGiSkPU4l3pg_iGdC&open=AZ1OGiSkPU4l3pg_iGdC&pullRequest=402

Check warning on line 278 in src/services/include/services/OAuthClient.hpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Pass large object "userinfoEndpoint" by reference to const.

See more on https://sonarcloud.io/project/issues?id=fair-acc_opencmw-cpp&issues=AZ1OGiSkPU4l3pg_iGdD&open=AZ1OGiSkPU4l3pg_iGdD&pullRequest=402

Check warning on line 278 in src/services/include/services/OAuthClient.hpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Pass large object "brokerAddress" by reference to const.

See more on https://sonarcloud.io/project/issues?id=fair-acc_opencmw-cpp&issues=AZ1OGiSkPU4l3pg_iGdE&open=AZ1OGiSkPU4l3pg_iGdE&pullRequest=402
: OAuthWorkerType(brokerAddress, {}, context, settings), _client(redirectUri, endpoint, tokenEndpoint, userinfoEndpoint), _keystore(brokerAddress, context, settings) { init(); };
template<typename BrokerType>
explicit OAuthWorker(StrictUri redirecturi, StrictUri endpoint, StrictUri tokenEndpoint, const BrokerType &broker)
: OAuthWorkerType(broker, {}), _client(redirecturi, endpoint, tokenEndpoint), _keystore(broker) { init(); };
explicit OAuthWorker(StrictUri redirecturi, StrictUri endpoint, StrictUri tokenEndpoint, StrictUri userinfoEndpoint, const BrokerType &broker)

Check warning on line 281 in src/services/include/services/OAuthClient.hpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Pass large object "userinfoEndpoint" by reference to const.

See more on https://sonarcloud.io/project/issues?id=fair-acc_opencmw-cpp&issues=AZ1OGiSkPU4l3pg_iGdJ&open=AZ1OGiSkPU4l3pg_iGdJ&pullRequest=402

Check warning on line 281 in src/services/include/services/OAuthClient.hpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Pass large object "endpoint" by reference to const.

See more on https://sonarcloud.io/project/issues?id=fair-acc_opencmw-cpp&issues=AZ1OGiSkPU4l3pg_iGdH&open=AZ1OGiSkPU4l3pg_iGdH&pullRequest=402

Check warning on line 281 in src/services/include/services/OAuthClient.hpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Pass large object "redirecturi" by reference to const.

See more on https://sonarcloud.io/project/issues?id=fair-acc_opencmw-cpp&issues=AZ1OGiSkPU4l3pg_iGdG&open=AZ1OGiSkPU4l3pg_iGdG&pullRequest=402

Check warning on line 281 in src/services/include/services/OAuthClient.hpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Pass large object "tokenEndpoint" by reference to const.

See more on https://sonarcloud.io/project/issues?id=fair-acc_opencmw-cpp&issues=AZ1OGiSkPU4l3pg_iGdI&open=AZ1OGiSkPU4l3pg_iGdI&pullRequest=402
: OAuthWorkerType(broker, {}), _client(redirecturi, endpoint, tokenEndpoint, userinfoEndpoint), _keystore(broker) { init(); };
~OAuthWorker() {
_keystore.shutdown();
_keystoreThread.join();
Expand Down Expand Up @@ -284,6 +321,9 @@
const auto access = _client.requestToken(code, in.clientId);
out.accessToken = access.accessToken._token;
out.refreshToken = access.refreshToken._token;

// get RBAC roles
out.roles = _client.getAssignedRoles(out.accessToken);
}
});
_keystoreThread = std::thread([this] { _keystore.run(); });
Expand Down
3 changes: 2 additions & 1 deletion src/services/test/OAuthClient_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ TEST_CASE("Worker test", "[OAuth]") {
opencmw::URI(redirectBase),
opencmw::URI(kcBase + "/realms/testrealm/protocol/openid-connect/auth"),
opencmw::URI(kcBase + "/realms/testrealm/protocol/openid-connect/token"),
broker
opencmw::URI(kcBase + "/realms/testrealm/protocol/openid-connect/userinfo"),
broker
};

REQUIRE(broker.bind(opencmw::URI<>("mds://127.0.0.1:12345")));
Expand Down
Loading