1
1
#include < Access/AccessTokenProcessor.h>
2
+ #include < Common/logger_useful.h>
2
3
#include < picojson/picojson.h>
3
4
#include < jwt-cpp/jwt.h>
4
5
@@ -38,13 +39,49 @@ namespace
38
39
39
40
return value.get <std::string>();
40
41
}
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
+ }
41
78
}
42
79
43
80
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" );
45
82
const Poco::URI GoogleAccessTokenProcessor::user_info_uri = Poco::URI(" https://www.googleapis.com/oauth2/v3/userinfo" );
46
83
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 " );
48
85
49
86
50
87
std::unique_ptr<IAccessTokenProcessor> IAccessTokenProcessor::parseTokenProcessor (
@@ -93,19 +130,24 @@ bool GoogleAccessTokenProcessor::resolveAndValidate(const TokenCredentials & cre
93
130
{
94
131
const String & token = credentials.getToken ();
95
132
96
- String user_name = tryGetUserName (token);
97
- if (user_name.empty ())
98
- throw Exception (ErrorCodes::AUTHENTICATION_FAILED, " Failed to authenticate with access token" );
99
-
100
133
auto user_info = getUserInfo (token);
134
+ String user_name = user_info[" sub" ];
101
135
102
136
if (email_regex.ok ())
103
137
{
104
138
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
+
106
144
// / Additionally validate user email to match regex from config.
107
145
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
+
109
151
}
110
152
// / Credentials are passed as const everywhere up the flow, so we have to comply,
111
153
// / in this case const_cast looks acceptable.
@@ -115,100 +157,61 @@ bool GoogleAccessTokenProcessor::resolveAndValidate(const TokenCredentials & cre
115
157
return true ;
116
158
}
117
159
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
-
147
160
std::unordered_map<String, String> GoogleAccessTokenProcessor::getUserInfo (const String & token) const
148
161
{
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);
165
164
166
165
try
167
166
{
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;
172
170
}
173
- catch (const std::runtime_error & e)
171
+ catch (std::runtime_error & e)
174
172
{
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 ());
176
174
}
177
175
}
178
176
177
+
179
178
bool AzureAccessTokenProcessor::resolveAndValidate (const TokenCredentials & credentials)
180
179
{
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
185
184
186
185
const String & token = credentials.getToken ();
187
186
188
187
try
189
188
{
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
+
191
199
}
192
200
catch (...)
193
201
{
194
202
return false ;
195
203
}
196
204
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.
209
206
const_cast <TokenCredentials &>(credentials).setGroups ({});
210
207
211
208
return true ;
212
209
}
213
210
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
+
214
217
}
0 commit comments