Kurlyk
Loading...
Searching...
No Matches
OAuthPkceClient.hpp
Go to the documentation of this file.
1#pragma once
2#ifndef KURLYK_HEADER_KURLYK_HTTP_AUTH_O_AUTH_PKCE_CLIENT_HPP_INCLUDED
3#define KURLYK_HEADER_KURLYK_HTTP_AUTH_O_AUTH_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
60
61 QueryParams params;
62 params.emplace("response_type", "code");
63 params.emplace("client_id", m_config.client_id);
64 params.emplace("redirect_uri", m_config.redirect_uri);
65 if (!m_config.scope.empty()) {
66 params.emplace("scope", m_config.scope);
67 }
68 params.emplace("state", m_state);
69
70 if (m_config.use_pkce) {
74 params.emplace("code_challenge", m_code_challenge);
75 params.emplace("code_challenge_method", pair.code_challenge_method);
76 }
77
78 if (!m_config.audience.empty()) {
79 params.emplace("audience", m_config.audience);
80 }
81 if (!m_config.prompt.empty()) {
82 params.emplace("prompt", m_config.prompt);
83 }
84 if (!m_config.access_type.empty()) {
85 params.emplace("access_type", m_config.access_type);
86 }
87
89 }
90
95 bool exchange_code(const std::string& code, AuthResult& out_result) {
96 out_result = AuthResult();
97
98 if (code.empty()) {
100 out_result.error_message = "authorization code is empty";
101 return false;
102 }
103
104 if (m_config.token_endpoint.empty()) {
105 out_result.error = AuthError::InvalidConfig;
106 out_result.error_message = "token_endpoint is not configured";
107 return false;
108 }
109
110 QueryParams body_params;
111 body_params.emplace("grant_type", "authorization_code");
112 body_params.emplace("code", code);
113 body_params.emplace("redirect_uri", m_config.redirect_uri);
114 body_params.emplace("client_id", m_config.client_id);
115 if (!m_config.client_secret.empty()) {
116 body_params.emplace("client_secret", m_config.client_secret);
117 }
118 if (m_config.use_pkce && !m_code_verifier.empty()) {
119 body_params.emplace("code_verifier", m_code_verifier);
120 }
121
122 const std::string body = utils::to_query_string(body_params);
123 Headers headers;
124 headers.emplace("Content-Type", "application/x-www-form-urlencoded");
125
126 auto request_result = http_post(m_config.token_endpoint, QueryParams(), headers, body);
127 (void)request_result.first;
128
129 HttpResponsePtr response;
130 try {
131 response = request_result.second.get();
132 } catch (const std::exception& e) {
133 out_result.error = AuthError::HttpError;
134 out_result.error_message = std::string("HTTP request failed: ") + e.what();
135 return false;
136 }
137
138 if (!response) {
139 out_result.error = AuthError::HttpError;
140 out_result.error_message = "HTTP request returned null response";
141 return false;
142 }
143
144 out_result.raw_response = response->content;
145
146 if (response->status_code < 200 || response->status_code >= 300) {
147 out_result.error = AuthError::HttpError;
148 out_result.error_message = "Token endpoint returned HTTP " +
149 std::to_string(response->status_code);
150 return false;
151 }
152
153 return parse_token_response(response->content, out_result);
154 }
155
160 bool refresh_access_token(const std::string& refresh_token, AuthResult& out_result) {
161 out_result = AuthResult();
162
163 if (refresh_token.empty()) {
164 out_result.error = AuthError::InvalidConfig;
165 out_result.error_message = "refresh_token is empty";
166 return false;
167 }
168
169 if (m_config.token_endpoint.empty()) {
170 out_result.error = AuthError::InvalidConfig;
171 out_result.error_message = "token_endpoint is not configured";
172 return false;
173 }
174
175 QueryParams body_params;
176 body_params.emplace("grant_type", "refresh_token");
177 body_params.emplace("refresh_token", refresh_token);
178 body_params.emplace("client_id", m_config.client_id);
179 if (!m_config.client_secret.empty()) {
180 body_params.emplace("client_secret", m_config.client_secret);
181 }
182
183 const std::string body = utils::to_query_string(body_params);
184 Headers headers;
185 headers.emplace("Content-Type", "application/x-www-form-urlencoded");
186
187 auto request_result = http_post(m_config.token_endpoint, QueryParams(), headers, body);
188 (void)request_result.first;
189
190 HttpResponsePtr response;
191 try {
192 response = request_result.second.get();
193 } catch (const std::exception& e) {
194 out_result.error = AuthError::HttpError;
195 out_result.error_message = std::string("HTTP request failed: ") + e.what();
196 return false;
197 }
198
199 if (!response) {
200 out_result.error = AuthError::HttpError;
201 out_result.error_message = "HTTP request returned null response";
202 return false;
203 }
204
205 out_result.raw_response = response->content;
206
207 if (response->status_code < 200 || response->status_code >= 300) {
208 out_result.error = AuthError::HttpError;
209 out_result.error_message = "Token endpoint returned HTTP " +
210 std::to_string(response->status_code);
211 return false;
212 }
213
214 return parse_token_response(response->content, out_result);
215 }
216
220 bool validate_state(const std::string& returned_state) const {
221 return !m_state.empty() && m_state == returned_state;
222 }
223
225 const std::string& code_verifier() const {
226 return m_code_verifier;
227 }
228
230 const std::string& state() const {
231 return m_state;
232 }
233
234 protected:
235 bool parse_token_response(const std::string& raw_response, AuthResult& out_result) {
236#if KURLYK_JSON_SUPPORT
237 try {
238 nlohmann::json j = nlohmann::json::parse(raw_response);
239
240 if (j.contains("error")) {
242 out_result.error_message = j.value("error_description",
243 j.value("error", "unknown error"));
244 return false;
245 }
246
247 OAuthToken token;
248 token.raw_response = raw_response;
249 if (j.contains("access_token")) token.access_token = j["access_token"].get<std::string>();
250 if (j.contains("refresh_token")) token.refresh_token = j["refresh_token"].get<std::string>();
251 if (j.contains("token_type")) token.token_type = j["token_type"].get<std::string>();
252 if (j.contains("scope")) token.scope = j["scope"].get<std::string>();
253
254 if (j.contains("expires_in")) {
255 int64_t expires_in = j["expires_in"].get<int64_t>();
256 auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
257 std::chrono::system_clock::now().time_since_epoch()).count();
258 token.expires_at_ms = now_ms + (expires_in * 1000);
259 }
260
261 if (token.access_token.empty()) {
263 out_result.error_message = "access_token is missing in response";
264 return false;
265 }
266
267 out_result.token = token;
268 out_result.success = true;
269 return true;
270 } catch (const std::exception& e) {
272 out_result.error_message = std::string("JSON parse error: ") + e.what();
273 return false;
274 }
275#else
276 if (m_token_parser) {
277 std::string error_msg;
278 bool ok = m_token_parser(raw_response, out_result.token, error_msg);
279 if (!ok) {
281 out_result.error_message = error_msg.empty() ? "custom token parser failed" : error_msg;
282 return false;
283 }
284 out_result.success = true;
285 return true;
286 }
287
289 out_result.error_message = "JSON support disabled and no custom token parser set";
290 return false;
291#endif
292 }
293
294 private:
296 std::string m_code_verifier;
297 std::string m_code_challenge;
298 std::string m_state;
300 };
301
302} // namespace auth
303} // namespace http
304} // namespace kurlyk
305
306#endif // KURLYK_HEADER_KURLYK_HTTP_AUTH_O_AUTH_PKCE_CLIENT_HPP_INCLUDED
Defines authentication result types and error codes.
Defines the OAuthConfig structure for OAuth2 client configuration.
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:106
std::string generate_code_verifier(std::size_t length=64)
Generates a cryptographically strong PKCE code verifier.
Definition pkce.hpp:69
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.
Provides PKCE (Proof Key for Code Exchange) utilities per RFC 7636.
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.
std::string prompt
Optional prompt parameter (e.g. "consent").
std::string client_secret
Client secret (optional, usually empty for public/PKCE clients).
std::string redirect_uri
Registered redirect URI.
std::string audience
Optional audience parameter.
std::string client_id
OAuth2 client identifier.
std::string access_type
Optional access_type parameter (e.g. "offline").
bool use_pkce
Whether to use PKCE (RFC 7636). Default true.
std::string authorization_endpoint
Authorization server URL.
std::string token_endpoint
Token exchange URL.
std::string scope
Space-separated requested scopes.
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:60
std::string code_challenge_method
Challenge method, always "S256".
Definition pkce.hpp:63
std::string code_verifier
Randomly generated code verifier.
Definition pkce.hpp:61
std::string code_challenge
Derived S256 code challenge.
Definition pkce.hpp:62