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" );
4582const 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
5087std::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-
147160std::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+
179178bool 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}
0 commit comments