Skip to content

Commit 10f04f6

Browse files
committed
resolve tokenCredentials on creation
1 parent 992ced5 commit 10f04f6

15 files changed

+167
-179
lines changed

src/Access/AccessTokenProcessor.cpp

Lines changed: 79 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include <Access/AccessTokenProcessor.h>
2+
#include <Common/logger_useful.h>
23
#include <picojson/picojson.h>
34
#include <jwt-cpp/jwt.h>
45

@@ -38,13 +39,49 @@ namespace
3839

3940
return value.get<std::string>();
4041
}
42+
43+
picojson::object getObjectFromURI(const Poco::URI & uri, const String & token = "")
44+
{
45+
Poco::Net::HTTPResponse response;
46+
std::ostringstream responseString;
47+
48+
Poco::Net::HTTPRequest request{Poco::Net::HTTPRequest::HTTP_GET, uri.getPathAndQuery()};
49+
if (!token.empty())
50+
request.add("Authorization", "Bearer " + token);
51+
52+
if (uri.getScheme() == "https") {
53+
Poco::Net::HTTPSClientSession session(uri.getHost(), uri.getPort());
54+
session.sendRequest(request);
55+
Poco::StreamCopier::copyStream(session.receiveResponse(response), responseString);
56+
}
57+
else
58+
{
59+
Poco::Net::HTTPClientSession session(uri.getHost(), uri.getPort());
60+
session.sendRequest(request);
61+
Poco::StreamCopier::copyStream(session.receiveResponse(response), responseString);
62+
}
63+
64+
if (response.getStatus() != Poco::Net::HTTPResponse::HTTP_OK)
65+
throw Exception(ErrorCodes::AUTHENTICATION_FAILED,
66+
"Failed to get user info by access token, code: {}, reason: {}", response.getStatus(),
67+
response.getReason());
68+
69+
try
70+
{
71+
return parseJSON(responseString.str());
72+
}
73+
catch (const std::runtime_error & e)
74+
{
75+
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Failed to parse server response: {}", e.what());
76+
}
77+
}
4178
}
4279

4380

44-
const Poco::URI GoogleAccessTokenProcessor::token_info_uri = Poco::URI("https://www.googleapis.com/oauth2/v3/tokeninfo");
81+
[[maybe_unused]] const Poco::URI GoogleAccessTokenProcessor::token_info_uri = Poco::URI("https://www.googleapis.com/oauth2/v3/tokeninfo");
4582
const Poco::URI GoogleAccessTokenProcessor::user_info_uri = Poco::URI("https://www.googleapis.com/oauth2/v3/userinfo");
4683

47-
const Poco::URI AzureAccessTokenProcessor::user_info_uri = Poco::URI("https://graph.microsoft.com/v1.0/me");
84+
const Poco::URI AzureAccessTokenProcessor::user_info_uri = Poco::URI("https://graph.microsoft.com/oidc/userinfo");
4885

4986

5087
std::unique_ptr<IAccessTokenProcessor> IAccessTokenProcessor::parseTokenProcessor(
@@ -93,19 +130,24 @@ bool GoogleAccessTokenProcessor::resolveAndValidate(const TokenCredentials & cre
93130
{
94131
const String & token = credentials.getToken();
95132

96-
String user_name = tryGetUserName(token);
97-
if (user_name.empty())
98-
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Failed to authenticate with access token");
99-
100133
auto user_info = getUserInfo(token);
134+
String user_name = user_info["sub"];
101135

102136
if (email_regex.ok())
103137
{
104138
if (!user_info.contains("email"))
105-
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Failed to authenticate user {}: e-mail address not found in user data.", user_name);
139+
{
140+
LOG_TRACE(getLogger("AccessTokenProcessor"), "{}: Failed to validate {} by e-mail", name, user_name);
141+
return false;
142+
}
143+
106144
/// Additionally validate user email to match regex from config.
107145
if (!RE2::FullMatch(user_info["email"], email_regex))
108-
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Failed to authenticate user {}: e-mail address is not permitted.", user_name);
146+
{
147+
LOG_TRACE(getLogger("AccessTokenProcessor"), "{}: Failed to authenticate user {}: e-mail address is not permitted.", name, user_name);
148+
return false;
149+
}
150+
109151
}
110152
/// Credentials are passed as const everywhere up the flow, so we have to comply,
111153
/// in this case const_cast looks acceptable.
@@ -115,100 +157,61 @@ bool GoogleAccessTokenProcessor::resolveAndValidate(const TokenCredentials & cre
115157
return true;
116158
}
117159

118-
String GoogleAccessTokenProcessor::tryGetUserName(const String & token) const
119-
{
120-
Poco::Net::HTTPSClientSession session(token_info_uri.getHost(), token_info_uri.getPort());
121-
122-
Poco::Net::HTTPRequest request{Poco::Net::HTTPRequest::HTTP_GET, token_info_uri.getPathAndQuery()};
123-
request.add("Authorization", "Bearer " + token);
124-
session.sendRequest(request);
125-
126-
Poco::Net::HTTPResponse response;
127-
std::istream & responseStream = session.receiveResponse(response);
128-
129-
if (response.getStatus() != Poco::Net::HTTPResponse::HTTP_OK)
130-
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Failed to resolve access token, code: {}, reason: {}", response.getStatus(), response.getReason());
131-
132-
std::ostringstream responseString;
133-
Poco::StreamCopier::copyStream(responseStream, responseString);
134-
135-
try
136-
{
137-
picojson::object parsed_json = parseJSON(responseString.str());
138-
String username = getValueByKey(parsed_json, "sub");
139-
return username;
140-
}
141-
catch (const std::runtime_error &)
142-
{
143-
return "";
144-
}
145-
}
146-
147160
std::unordered_map<String, String> GoogleAccessTokenProcessor::getUserInfo(const String & token) const
148161
{
149-
std::unordered_map<String, String> user_info;
150-
151-
Poco::Net::HTTPSClientSession session(user_info_uri.getHost(), user_info_uri.getPort());
152-
153-
Poco::Net::HTTPRequest request{Poco::Net::HTTPRequest::HTTP_GET, user_info_uri.getPathAndQuery()};
154-
request.add("Authorization", "Bearer " + token);
155-
session.sendRequest(request);
156-
157-
Poco::Net::HTTPResponse response;
158-
std::istream & responseStream = session.receiveResponse(response);
159-
160-
if (response.getStatus() != Poco::Net::HTTPResponse::HTTP_OK)
161-
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Failed to get user info by access token, code: {}, reason: {}", response.getStatus(), response.getReason());
162-
163-
std::ostringstream responseString;
164-
Poco::StreamCopier::copyStream(responseStream, responseString);
162+
std::unordered_map<String, String> user_info_map;
163+
picojson::object user_info_json = getObjectFromURI(user_info_uri, token);
165164

166165
try
167166
{
168-
picojson::object parsed_json = parseJSON(responseString.str());
169-
user_info["email"] = getValueByKey(parsed_json, "email");
170-
user_info["sub"] = getValueByKey(parsed_json, "sub");
171-
return user_info;
167+
user_info_map["email"] = getValueByKey(user_info_json, "email");
168+
user_info_map["sub"] = getValueByKey(user_info_json, "sub");
169+
return user_info_map;
172170
}
173-
catch (const std::runtime_error & e)
171+
catch (std::runtime_error & e)
174172
{
175-
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Failed to get user info by access token: {}", e.what());
173+
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "{}: Failed to get user info with token: {}", name, e.what());
176174
}
177175
}
178176

177+
179178
bool AzureAccessTokenProcessor::resolveAndValidate(const TokenCredentials & credentials)
180179
{
181-
/// Token is a JWT in this case, all we need is to decode it and verify against JWKS (similar to JWTValidator.h)
182-
String user_name = credentials.getUserName();
183-
if (user_name.empty())
184-
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Failed to authenticate with access token: cannot extract username");
180+
/// Token is a JWT in this case, but we cannot directly verify it against Azure AD JWKS. We will not trust any data in this token.
181+
/// e.g. see here: https://stackoverflow.com/questions/60778634/failing-signature-validation-of-jwt-tokens-from-azure-ad
182+
/// Let Azure validate it: only valid tokens will be accepted.
183+
/// Use GET https://graph.microsoft.com/oidc/userinfo to verify token and get sub at the same time
185184

186185
const String & token = credentials.getToken();
187186

188187
try
189188
{
190-
token_validator->validate("", token);
189+
String username = validateTokenAndGetUsername(token);
190+
if (!username.empty())
191+
{
192+
/// Credentials are passed as const everywhere up the flow, so we have to comply,
193+
/// in this case const_cast looks acceptable.
194+
const_cast<TokenCredentials &>(credentials).setUserName(username);
195+
}
196+
else
197+
LOG_TRACE(getLogger("AccessTokenProcessor"), "{}: Failed to get username with token", name);
198+
191199
}
192200
catch (...)
193201
{
194202
return false;
195203
}
196204

197-
const auto decoded_token = jwt::decode(token);
198-
199-
if (email_regex.ok())
200-
{
201-
if (!decoded_token.has_payload_claim("email"))
202-
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Failed to authenticate user {}: e-mail address not found in user data.", user_name);
203-
/// Additionally validate user email to match regex from config.
204-
if (!RE2::FullMatch(decoded_token.get_payload_claim("email").as_string(), email_regex))
205-
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Failed to authenticate user {}: e-mail address is not permitted.", user_name);
206-
}
207-
/// Credentials are passed as const everywhere up the flow, so we have to comply,
208-
/// in this case const_cast looks acceptable.
205+
/// TODO: do not store it in credentials.
209206
const_cast<TokenCredentials &>(credentials).setGroups({});
210207

211208
return true;
212209
}
213210

211+
String AzureAccessTokenProcessor::validateTokenAndGetUsername(const String & token) const
212+
{
213+
picojson::object user_info_json = getObjectFromURI(user_info_uri, token);
214+
return getValueByKey(user_info_json, "sub");
215+
}
216+
214217
}

src/Access/AccessTokenProcessor.h

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,9 @@ class GoogleAccessTokenProcessor : public IAccessTokenProcessor
6969
bool resolveAndValidate(const TokenCredentials & credentials) override;
7070

7171
private:
72-
static const Poco::URI token_info_uri;
72+
[[maybe_unused]] static const Poco::URI token_info_uri;
7373
static const Poco::URI user_info_uri;
7474

75-
String tryGetUserName(const String & token) const;
76-
7775
std::unordered_map<String, String> getUserInfo(const String & token) const;
7876
};
7977

@@ -85,16 +83,12 @@ class AzureAccessTokenProcessor : public IAccessTokenProcessor
8583
const String & email_regex_str,
8684
const String & client_id_,
8785
const String & tenant_id_,
88-
const String & client_secret_,
89-
const size_t jwks_refresh_interval = 300000)
86+
const String & client_secret_)
9087
: IAccessTokenProcessor(name_, email_regex_str),
9188
client_id(client_id_),
9289
tenant_id(tenant_id_),
9390
client_secret(client_secret_),
94-
jwks_uri_str("https://login.microsoftonline.com/" + tenant_id + "/discovery/v2.0/keys")
95-
{
96-
token_validator = std::make_unique<JWKSValidator>(name + "_jwks_validator", std::make_unique<JWKSClient>(jwks_uri_str, jwks_refresh_interval));
97-
}
91+
jwks_uri_str("https://login.microsoftonline.com/" + tenant_id + "/discovery/v2.0/keys") {}
9892

9993
bool resolveAndValidate(const TokenCredentials & credentials) override;
10094
private:
@@ -106,7 +100,7 @@ class AzureAccessTokenProcessor : public IAccessTokenProcessor
106100

107101
const String jwks_uri_str;
108102

109-
std::unique_ptr<JWKSValidator> token_validator;
103+
String validateTokenAndGetUsername(const String & token) const;
110104
};
111105

112106
}

src/Access/Authentication.cpp

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -252,15 +252,10 @@ bool Authentication::areCredentialsValid(
252252
if (authentication_method.getType() != AuthenticationType::JWT)
253253
return false;
254254

255-
if (token_credentials->isJWT())
256-
{
257-
/// The token was parsed as JWT, no further action needed.
258-
return external_authenticators.checkJWTCredentials(authentication_method.getJWTClaims(), *token_credentials);
259-
}
260-
else
261-
{
262-
return external_authenticators.checkAccessTokenCredentials(*token_credentials);
263-
}
255+
if (external_authenticators.checkJWTClaims(authentication_method.getJWTClaims(), *token_credentials))
256+
return true;
257+
258+
return external_authenticators.checkAccessTokenCredentials(*token_credentials);
264259
}
265260

266261
if ([[maybe_unused]] const auto * always_allow_credentials = typeid_cast<const AlwaysAllowCredentials *>(&credentials))

src/Access/Common/JWKSProvider.cpp

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
#include <Access/Common/JWKSProvider.h>
22

33
#include <Common/Exception.h>
4-
#include <Common/logger_useful.h>
54
#include <Poco/StreamCopier.h>
65
#include <fstream>
76

8-
#include <Common/logger_useful.h>
9-
107

118
namespace DB
129
{

src/Access/Credentials.cpp

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -100,34 +100,7 @@ const String & BasicCredentials::getPassword() const
100100
return password;
101101
}
102102

103-
namespace
104-
{
105-
String extractUsernameFromToken(const String & token)
106-
{
107-
try
108-
{
109-
/// Attempt to handle token as JWT.
110-
auto decoded_jwt = jwt::decode(token);
111-
return decoded_jwt.get_subject();
112-
}
113-
catch (...)
114-
{
115-
/// Token is not JWT, try to handle it as access token
116-
return "";
117-
}
118-
}
119-
}
120-
121-
TokenCredentials::TokenCredentials(const String & token_)
122-
: Credentials(extractUsernameFromToken(token_))
123-
, token(token_)
124-
{
125-
// If username is empty, then the token is probably not JWT;
126-
// we will try treating this token as an access token.
127-
if (!user_name.empty())
128-
{
129-
is_ready = true;
130-
is_jwt = true;
131-
}
132-
}
103+
/// Unless the token is validated, we will not use any data from it, including username.
104+
TokenCredentials::TokenCredentials(const String & token_) : Credentials(""), token(token_) {}
105+
133106
}

src/Access/Credentials.h

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,6 @@ class TokenCredentials : public Credentials
146146
is_ready = true;
147147
}
148148
}
149-
bool isJWT() const
150-
{
151-
return is_jwt;
152-
}
153149
std::set<String> getGroups() const
154150
{
155151
return groups;
@@ -160,7 +156,6 @@ class TokenCredentials : public Credentials
160156
}
161157
private:
162158
String token;
163-
bool is_jwt = false;
164159
std::set<String> groups;
165160
};
166161

0 commit comments

Comments
 (0)