DeterministicESPAsyncWebServer v6.28.0
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
oauth2.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file oauth2.cpp
6 * @brief OAuth2 token-endpoint client - implementation (no heap, no stdlib).
7 */
8
11
12#if DETWS_ENABLE_OAUTH2
13
15#include <string.h>
16
17namespace
18{
19// Bounded form-body builder.
20struct Buf
21{
22 char *o;
23 size_t cap;
24 size_t n;
25 bool ok;
26};
27
28void put_raw(Buf &b, const char *s)
29{
30 for (; *s; s++)
31 {
32 if (b.n + 1 >= b.cap)
33 {
34 b.ok = false;
35 return;
36 }
37 b.o[b.n++] = *s;
38 }
39}
40
41bool unreserved(char c)
42{
43 return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.' ||
44 c == '_' || c == '~';
45}
46
47// Percent-encode a value (application/x-www-form-urlencoded; unreserved pass).
48void put_enc(Buf &b, const char *s)
49{
50 for (; *s; s++)
51 {
52 unsigned char c = (unsigned char)*s;
53 if (unreserved((char)c))
54 {
55 if (b.n + 1 >= b.cap)
56 {
57 b.ok = false;
58 return;
59 }
60 b.o[b.n++] = (char)c;
61 }
62 else
63 {
64 if (b.n + 3 >= b.cap)
65 {
66 b.ok = false;
67 return;
68 }
69 b.o[b.n++] = '%';
70 b.o[b.n++] = det_hex_digit((c >> 4) & 0xF, true);
71 b.o[b.n++] = det_hex_digit(c & 0xF, true);
72 }
73 }
74}
75
76// Append "&key=<encoded value>".
77void put_param(Buf &b, const char *key, const char *val)
78{
79 put_raw(b, "&");
80 put_raw(b, key);
81 put_raw(b, "=");
82 put_enc(b, val);
83}
84
85int finish(Buf &b)
86{
87 if (!b.ok || b.n >= b.cap)
88 return 0;
89 b.o[b.n] = '\0';
90 return (int)b.n;
91}
92} // namespace
93
94int detws_oauth2_build_code_request(const char *code, const char *redirect_uri, const char *client_id,
95 const char *client_secret, const char *code_verifier, char *out, size_t cap)
96{
97 if (!code || !redirect_uri || !client_id || !out || cap == 0)
98 return 0;
99 Buf b = {out, cap, 0, true};
100 put_raw(b, "grant_type=authorization_code");
101 put_param(b, "code", code);
102 put_param(b, "redirect_uri", redirect_uri);
103 put_param(b, "client_id", client_id);
104 if (client_secret)
105 put_param(b, "client_secret", client_secret);
106 if (code_verifier)
107 put_param(b, "code_verifier", code_verifier);
108 return finish(b);
109}
110
111int detws_oauth2_build_refresh_request(const char *refresh_token, const char *client_id, const char *client_secret,
112 char *out, size_t cap)
113{
114 if (!refresh_token || !client_id || !out || cap == 0)
115 return 0;
116 Buf b = {out, cap, 0, true};
117 put_raw(b, "grant_type=refresh_token");
118 put_param(b, "refresh_token", refresh_token);
119 put_param(b, "client_id", client_id);
120 if (client_secret)
121 put_param(b, "client_secret", client_secret);
122 return finish(b);
123}
124
125bool detws_oauth2_parse_token_response(const char *json, DetwsOAuth2Tokens *out)
126{
127 if (!json || !out)
128 return false;
129 out->access_token[0] = '\0';
130 out->id_token[0] = '\0';
131 out->refresh_token[0] = '\0';
132 out->token_type[0] = '\0';
133 out->expires_in = 0;
134
135 if (!json_get_str(json, "access_token", out->access_token, sizeof(out->access_token)))
136 return false; // an error response (e.g. {"error":"invalid_grant"}) has no access_token
137 json_get_str(json, "id_token", out->id_token, sizeof(out->id_token));
138 json_get_str(json, "refresh_token", out->refresh_token, sizeof(out->refresh_token));
139 json_get_str(json, "token_type", out->token_type, sizeof(out->token_type));
140 long e = 0;
141 if (json_get_int(json, "expires_in", &e))
142 out->expires_in = e;
143 return true;
144}
145
146#if DETWS_ENABLE_HTTP_CLIENT
147
149
150namespace
151{
152// All OAuth2 exchange scratch, owned by one instance (internal linkage): the request-body
153// and response buffers (kept off the caller's stack), grouped so it is one named owner,
154// unreachable cross-TU.
155struct Oauth2Ctx
156{
157 char body[DETWS_OAUTH2_BODY_BUF];
158 char resp[DETWS_OAUTH2_RESP_BUF];
159};
160Oauth2Ctx s_oauth;
161
162int post_and_parse(Oauth2Ctx &c, const char *token_url, int body_len, DetwsOAuth2Tokens *out)
163{
164 if (body_len <= 0)
165 return (int)DetwsOAuth2Result::DETWS_OAUTH2_ERR_BUILD;
166 HttpClientResult r;
167 int st = http_post(token_url, "application/x-www-form-urlencoded", (const uint8_t *)c.body, (size_t)body_len, &r);
168 if (st <= 0)
169 return (int)DetwsOAuth2Result::DETWS_OAUTH2_ERR_TRANSPORT;
170 size_t k = r.body_len < sizeof(c.resp) - 1 ? r.body_len : sizeof(c.resp) - 1;
171 if (r.body && k)
172 memcpy(c.resp, r.body, k);
173 c.resp[k] = '\0';
174 if (!detws_oauth2_parse_token_response(c.resp, out))
175 return st >= 400
176 ? st
177 : (int)DetwsOAuth2Result::DETWS_OAUTH2_ERR_RESPONSE; // surface the provider's 4xx, else generic
178 return st;
179}
180} // namespace
181
182int detws_oauth2_exchange_code(const char *token_url, const char *code, const char *redirect_uri, const char *client_id,
183 const char *client_secret, const char *code_verifier, DetwsOAuth2Tokens *out)
184{
185 int n = detws_oauth2_build_code_request(code, redirect_uri, client_id, client_secret, code_verifier, s_oauth.body,
186 sizeof(s_oauth.body));
187 return post_and_parse(s_oauth, token_url, n, out);
188}
189
190int detws_oauth2_refresh(const char *token_url, const char *refresh_token, const char *client_id,
191 const char *client_secret, DetwsOAuth2Tokens *out)
192{
193 int n =
194 detws_oauth2_build_refresh_request(refresh_token, client_id, client_secret, s_oauth.body, sizeof(s_oauth.body));
195 return post_and_parse(s_oauth, token_url, n, out);
196}
197
198#endif // DETWS_ENABLE_HTTP_CLIENT
199
200#endif // DETWS_ENABLE_OAUTH2
Tiny no-stdlib hex encode / decode / nibble helpers (one shared copy).
char det_hex_digit(int nibble, bool upper=false)
Nibble 0..15 -> hex char (lowercase, or uppercase when upper).
Definition hex.h:24
Zero-heap outbound HTTP(S) client over raw lwIP (DETWS_ENABLE_HTTP_CLIENT).
bool json_get_str(const char *json, const char *key, char *out, size_t out_cap)
Read a top-level string member from a JSON object body.
Definition json.cpp:512
bool json_get_int(const char *json, const char *key, long *out)
Read a top-level integer member from a JSON object body.
Definition json.cpp:552
Layer 6 (Presentation) - zero-heap JSON: a bounded writer and top-level reader.
OAuth2 token-endpoint client - authorization-code + refresh (DETWS_ENABLE_OAUTH2).