Kurlyk
Loading...
Searching...
No Matches
OAuthPkceClient.hpp
Go to the documentation of this file.
1#pragma once
2#ifndef _KURLYK_HTTP_AUTH_OAUTH_PKCE_CLIENT_HPP_INCLUDED
3#define _KURLYK_HTTP_AUTH_OAUTH_PKCE_CLIENT_HPP_INCLUDED
4
7
9#include "data/AuthResult.hpp"
10#include "kurlyk/http/utils.hpp"
13#include "kurlyk/utils/Pkce.hpp"
14#include <functional>
15#include <chrono>
16#include <sstream>
17
18#if KURLYK_JSON_SUPPORT
19# include <nlohmann/json.hpp>
20#endif
21
22namespace kurlyk {
23namespace http {
24namespace auth {
25
32 public:
34 using TokenParser = std::function<bool(
35 const std::string& raw_response,
36 OAuthToken& out_token,
37 std::string& out_error)>;
38
41 explicit OAuthPkceClient(const OAuthConfig& config)
42 : m_config(config) {}
43
47 m_token_parser = parser;
48 }
49
53 if (m_config.authorization_endpoint.empty() ||
54 m_config.client_id.empty() ||
55 m_config.redirect_uri.empty()) {
56 return std::string();
57 }
58
59 if (m_state.empty()) {
61 }
62
63 QueryParams params;
64 params.emplace("response_type", "code");
65 params.emplace("client_id", m_config.client_id);
66 params.emplace("redirect_uri", m_config.redirect_uri);
67 if (!m_config.scope.empty()) {
68 params.emplace("scope", m_config.scope);
69 }
70 params.emplace("state", m_state);
71
72 if (m_config.use_pkce) {
76 params.emplace("code_challenge", m_code_challenge);
77 params.emplace("code_challenge_method", pair.code_challenge_method);
78 }
79
80 if (!m_config.audience.empty()) {
81 params.emplace("audience", m_config.audience);
82 }
83 if (!m_config.prompt.empty()) {
84 params.emplace("prompt", m_config.prompt);
85 }
86 if (!m_config.access_type.empty()) {
87 params.emplace("access_type", m_config.access_type);
88 }
89
90 return m_config.authorization_endpoint + utils::to_query_string(params, "?");
91 }
92
97 bool exchange_code(const std::string& code, AuthResult& out_result) {
98 out_result = AuthResult();
99
100 if (code.empty()) {
101 out_result.error = AuthError::InvalidConfig;
102 out_result.error_message = "authorization code is empty";
103 return false;
104 }
105
106 if (m_config.token_endpoint.empty()) {
107 out_result.error = AuthError::InvalidConfig;
108 out_result.error_message = "token_endpoint is not configured";
109 return false;
110 }
111
112 QueryParams body_params;
113 body_params.emplace("grant_type", "authorization_code");
114 body_params.emplace("code", code);
115 body_params.emplace("redirect_uri", m_config.redirect_uri);
116 body_params.emplace("client_id", m_config.client_id);
117 if (!m_config.client_secret.empty()) {
118 body_params.emplace("client_secret", m_config.client_secret);
119 }
120 if (m_config.use_pkce && !m_code_verifier.empty()) {
121 body_params.emplace("code_verifier", m_code_verifier);
122 }
123
124 const std::string body = utils::to_query_string(body_params);
125 Headers headers;
126 headers.emplace("Content-Type", "application/x-www-form-urlencoded");
127
128 auto request_result = http_post(m_config.token_endpoint, QueryParams(), headers, body);
129 (void)request_result.first;
130
131 HttpResponsePtr response;
132 try {
133 response = request_result.second.get();
134 } catch (const std::exception& e) {
135 out_result.error = AuthError::HttpError;
136 out_result.error_message = std::string("HTTP request failed: ") + e.what();
137 return false;
138 }
139
140 if (!response) {
141 out_result.error = AuthError::HttpError;
142 out_result.error_message = "HTTP request returned null response";
143 return false;
144 }
145
146 out_result.raw_response = response->content;
147
148 if (response->status_code < 200 || response->status_code >= 300) {
149 out_result.error = AuthError::HttpError;
150 out_result.error_message = "Token endpoint returned HTTP " +
151 std::to_string(response->status_code);
152 return false;
153 }
154
155 return parse_token_response(response->content, out_result);
156 }
157
162 bool refresh_access_token(const std::string& refresh_token, AuthResult& out_result) {
163 out_result = AuthResult();
164
165 if (refresh_token.empty()) {
166 out_result.error = AuthError::InvalidConfig;
167 out_result.error_message = "refresh_token is empty";
168 return false;
169 }
170
171 if (m_config.token_endpoint.empty()) {
172 out_result.error = AuthError::InvalidConfig;
173 out_result.error_message = "token_endpoint is not configured";
174 return false;
175 }
176
177 QueryParams body_params;
178 body_params.emplace("grant_type", "refresh_token");
179 body_params.emplace("refresh_token", refresh_token);
180 body_params.emplace("client_id", m_config.client_id);
181 if (!m_config.client_secret.empty()) {
182 body_params.emplace("client_secret", m_config.client_secret);
183 }
184
185 const std::string body = utils::to_query_string(body_params);
186 Headers headers;
187 headers.emplace("Content-Type", "application/x-www-form-urlencoded");
188
189 auto request_result = http_post(m_config.token_endpoint, QueryParams(), headers, body);
190 (void)request_result.first;
191
192 HttpResponsePtr response;
193 try {
194 response = request_result.second.get();
195 } catch (const std::exception& e) {
196 out_result.error = AuthError::HttpError;
197 out_result.error_message = std::string("HTTP request failed: ") + e.what();
198 return false;
199 }
200
201 if (!response) {
202 out_result.error = AuthError::HttpError;
203 out_result.error_message = "HTTP request returned null response";
204 return false;
205 }
206
207 out_result.raw_response = response->content;
208
209 if (response->status_code < 200 || response->status_code >= 300) {
210 out_result.error = AuthError::HttpError;
211 out_result.error_message = "Token endpoint returned HTTP " +
212 std::to_string(response->status_code);
213 return false;
214 }
215
216 return parse_token_response(response->content, out_result);
217 }
218
222 bool validate_state(const std::string& returned_state) const {
223 return !m_state.empty() && m_state == returned_state;
224 }
225
227 const std::string& code_verifier() const {
228 return m_code_verifier;
229 }
230
232 const std::string& state() const {
233 return m_state;
234 }
235
236 protected:
237 bool parse_token_response(const std::string& raw_response, AuthResult& out_result) {
238#if KURLYK_JSON_SUPPORT
239 try {
240 nlohmann::json j = nlohmann::json::parse(raw_response);
241
242 if (j.contains("error")) {
244 out_result.error_message = j.value("error_description",
245 j.value("error", "unknown error"));
246 return false;
247 }
248
249 OAuthToken token;
250 token.raw_response = raw_response;
251 if (j.contains("access_token")) token.access_token = j["access_token"].get<std::string>();
252 if (j.contains("refresh_token")) token.refresh_token = j["refresh_token"].get<std::string>();
253 if (j.contains("token_type")) token.token_type = j["token_type"].get<std::string>();
254 if (j.contains("scope")) token.scope = j["scope"].get<std::string>();
255
256 if (j.contains("expires_in")) {
257 int64_t expires_in = j["expires_in"].get<int64_t>();
258 auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
259 std::chrono::system_clock::now().time_since_epoch()).count();
260 token.expires_at_ms = now_ms + (expires_in * 1000);
261 }
262
263 if (token.access_token.empty()) {
265 out_result.error_message = "access_token is missing in response";
266 return false;
267 }
268
269 out_result.token = token;
270 out_result.success = true;
271 return true;
272 } catch (const std::exception& e) {
274 out_result.error_message = std::string("JSON parse error: ") + e.what();
275 return false;
276 }
277#else
278 if (m_token_parser) {
279 std::string error_msg;
280 bool ok = m_token_parser(raw_response, out_result.token, error_msg);
281 if (!ok) {
283 out_result.error_message = error_msg.empty() ? "custom token parser failed" : error_msg;
284 return false;
285 }
286 out_result.success = true;
287 return true;
288 }
289
291 out_result.error_message = "JSON support disabled and no custom token parser set";
292 return false;
293#endif
294 }
295
296 private:
298 std::string m_code_verifier;
299 std::string m_code_challenge;
300 std::string m_state;
302 };
303
304} // namespace auth
305} // namespace http
306} // namespace kurlyk
307
308#endif // _KURLYK_HTTP_AUTH_OAUTH_PKCE_CLIENT_HPP_INCLUDED
Defines authentication result types and error codes.
Defines the OAuthConfig structure for OAuth2 client configuration.
Provides PKCE (Proof Key for Code Exchange) utilities per RFC 7636.
bool exchange_code(const std::string &code, AuthResult &out_result)
Exchanges an authorization code for an access token.
std::function< bool( const std::string &raw_response, OAuthToken &out_token, std::string &out_error)> TokenParser
Callback type for custom token parsing when JSON support is disabled.
std::string build_authorization_url()
Builds the full authorization URL with query parameters.
bool refresh_access_token(const std::string &refresh_token, AuthResult &out_result)
Refreshes an access token using a refresh token.
const std::string & state() const
Returns the state parameter used in the last authorization request.
bool validate_state(const std::string &returned_state) const
Validates the state parameter returned by the authorization server.
void set_token_parser(TokenParser parser)
Sets a custom token parser for non-JSON builds.
const std::string & code_verifier() const
Returns the code verifier used in the last PKCE exchange.
OAuthPkceClient(const OAuthConfig &config)
Constructs a client with the given OAuth configuration.
bool parse_token_response(const std::string &raw_response, AuthResult &out_result)
Contains utility functions for handling HTTP requests, rate limiting, and request cancellation.
Provides utility functions for parsing HTTP headers and cookies.
std::string to_query_string(const QueryParams &query, const std::string &prefix=std::string()) noexcept
Converts a map of query parameters into a URL query string.
PkcePair make_pkce_pair()
Creates a PKCE pair with a freshly generated verifier.
Definition Pkce.hpp:59
std::string generate_code_verifier(std::size_t length=64)
Generates a cryptographically strong PKCE code verifier.
Definition Pkce.hpp:30
Primary namespace for the Kurlyk library, encompassing initialization, request management,...
std::unique_ptr< HttpResponse > HttpResponsePtr
Owning pointer to an HTTP response.
utils::CaseInsensitiveMultimap Headers
Alias for HTTP headers, providing a case-insensitive unordered multimap.
@ HttpError
HTTP request failed (non-2xx status or transport error).
@ InvalidResponse
Server response could not be parsed.
@ InvalidConfig
Missing or invalid client configuration.
@ UnsupportedFlow
The requested OAuth flow is not supported.
utils::CaseInsensitiveMultimap QueryParams
Alias for query parameters in HTTP requests, stored case-insensitively.
uint64_t http_post(const std::string &url, const QueryParams &query, const Headers &headers, const std::string &content, HttpResponseCallback callback)
Sends an asynchronous HTTP POST request with a callback.
Definition utils.hpp:489
Provides functions for percent-encoding and decoding strings.
Encapsulates the outcome of an authentication operation.
std::string error_message
Human-readable error description (if any).
AuthError error
Error classification.
std::string raw_response
Raw server response body for diagnostics.
bool success
Whether the operation succeeded.
OAuthToken token
Token data on success (may be partially filled on failure).
Stores client configuration for an OAuth2 Authorization Code + PKCE flow.
Holds the result of an OAuth2 token exchange.
int64_t expires_at_ms
Absolute expiration time in milliseconds since epoch (0 = unknown).
std::string raw_response
Raw server response body for debugging.
std::string scope
Granted scope (may be empty).
std::string access_token
The access token string.
std::string token_type
Token type, typically "Bearer".
std::string refresh_token
The refresh token string (may be empty).
Stores PKCE verifier and challenge values.
Definition Pkce.hpp:21
std::string code_challenge_method
Challenge method, always "S256".
Definition Pkce.hpp:24
std::string code_verifier
Randomly generated code verifier.
Definition Pkce.hpp:22
std::string code_challenge
Derived S256 code challenge.
Definition Pkce.hpp:23