ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
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 PC_ENABLE_OAUTH2
13
15#include <string.h>
16
17#if PC_ENABLE_HTTP_CLIENT
19#endif
20namespace
21{
22// Bounded form-body builder.
23struct Buf
24{
25 char *o;
26 size_t cap;
27 size_t n;
28 bool ok;
29};
30
31void put_raw(Buf &b, const char *s)
32{
33 for (; *s; s++)
34 {
35 if (b.n + 1 >= b.cap)
36 {
37 b.ok = false;
38 return;
39 }
40 b.o[b.n++] = *s;
41 }
42}
43
44bool unreserved(char c)
45{
46 return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.' ||
47 c == '_' || c == '~';
48}
49
50// Percent-encode a value (application/x-www-form-urlencoded; unreserved pass).
51void put_enc(Buf &b, const char *s)
52{
53 for (; *s; s++)
54 {
55 unsigned char c = (unsigned char)*s;
56 if (unreserved((char)c))
57 {
58 if (b.n + 1 >= b.cap)
59 {
60 b.ok = false;
61 return;
62 }
63 b.o[b.n++] = (char)c;
64 }
65 else
66 {
67 if (b.n + 3 >= b.cap)
68 {
69 b.ok = false;
70 return;
71 }
72 b.o[b.n++] = '%';
73 b.o[b.n++] = pc_hex_digit((c >> 4) & 0xF, true);
74 b.o[b.n++] = pc_hex_digit(c & 0xF, true);
75 }
76 }
77}
78
79// Append "&key=<encoded value>".
80void put_param(Buf &b, const char *key, const char *val)
81{
82 put_raw(b, "&");
83 put_raw(b, key);
84 put_raw(b, "=");
85 put_enc(b, val);
86}
87
88int finish(Buf &b)
89{
90 if (!b.ok || b.n >= b.cap) // GCOVR_EXCL_BR_LINE b.n>=b.cap while b.ok is true is unreachable: both
91 {
92 return 0; // public builders reject cap==0 before constructing Buf, so b.n=0 < b.cap
93 // holds at construction, and every put_raw/put_enc write only commits
94 // after checking b.n+1 (or +3) < b.cap, so b.n < b.cap stays invariant
95 // for as long as b.ok remains true.
96 }
97 b.o[b.n] = '\0';
98 return (int)b.n;
99}
100} // namespace
101
102int pc_oauth2_build_code_request(const char *code, const char *redirect_uri, const char *client_id,
103 const char *client_secret, const char *code_verifier, char *out, size_t cap)
104{
105 if (!code || !redirect_uri || !client_id || !out || cap == 0)
106 {
107 return 0;
108 }
109 Buf b = {out, cap, 0, true};
110 put_raw(b, "grant_type=authorization_code");
111 put_param(b, "code", code);
112 put_param(b, "redirect_uri", redirect_uri);
113 put_param(b, "client_id", client_id);
114 if (client_secret)
115 {
116 put_param(b, "client_secret", client_secret);
117 }
118 if (code_verifier)
119 {
120 put_param(b, "code_verifier", code_verifier);
121 }
122 return finish(b);
123}
124
125int pc_oauth2_build_refresh_request(const char *refresh_token, const char *client_id, const char *client_secret,
126 char *out, size_t cap)
127{
128 if (!refresh_token || !client_id || !out || cap == 0)
129 {
130 return 0;
131 }
132 Buf b = {out, cap, 0, true};
133 put_raw(b, "grant_type=refresh_token");
134 put_param(b, "refresh_token", refresh_token);
135 put_param(b, "client_id", client_id);
136 if (client_secret)
137 {
138 put_param(b, "client_secret", client_secret);
139 }
140 return finish(b);
141}
142
143bool pc_oauth2_parse_token_response(const char *json, pc_o_auth2_tokens *out)
144{
145 if (!json || !out)
146 {
147 return false;
148 }
149 out->access_token[0] = '\0';
150 out->id_token[0] = '\0';
151 out->refresh_token[0] = '\0';
152 out->token_type[0] = '\0';
153 out->expires_in = 0;
154
155 if (!json_get_str(json, "access_token", out->access_token, sizeof(out->access_token)))
156 {
157 return false; // an error response (e.g. {"error":"invalid_grant"}) has no access_token
158 }
159 json_get_str(json, "id_token", out->id_token, sizeof(out->id_token));
160 json_get_str(json, "refresh_token", out->refresh_token, sizeof(out->refresh_token));
161 json_get_str(json, "token_type", out->token_type, sizeof(out->token_type));
162 long e = 0;
163 if (json_get_int(json, "expires_in", &e))
164 {
165 out->expires_in = e;
166 }
167 return true;
168}
169
170#if PC_ENABLE_HTTP_CLIENT
171
172namespace
173{
174// All OAuth2 exchange scratch, owned by one instance (internal linkage): the request-body
175// and response buffers (kept off the caller's stack), grouped so it is one named owner,
176// unreachable cross-TU.
177struct Oauth2Ctx
178{
179 char body[PC_OAUTH2_BODY_BUF];
180 char resp[PC_OAUTH2_RESP_BUF];
181};
182Oauth2Ctx s_oauth;
183
184int post_and_parse(Oauth2Ctx &c, const char *token_url, int body_len, pc_o_auth2_tokens *out)
185{
186 if (body_len <= 0)
187 {
188 return (int)pc_o_auth2_result::PC_OAUTH2_ERR_BUILD;
189 }
190 HttpClientResult r;
191 int st = http_post(token_url, "application/x-www-form-urlencoded", (const uint8_t *)c.body, (size_t)body_len, &r);
192 if (st <= 0)
193 {
194 return (int)pc_o_auth2_result::PC_OAUTH2_ERR_TRANSPORT;
195 }
196 size_t k = r.body_len < sizeof(c.resp) - 1 ? r.body_len : sizeof(c.resp) - 1;
197 if (r.body && k)
198 {
199 memcpy(c.resp, r.body, k);
200 }
201 c.resp[k] = '\0';
202 if (!pc_oauth2_parse_token_response(c.resp, out))
203 {
204 return st >= 400 ? st
205 : (int)pc_o_auth2_result::PC_OAUTH2_ERR_RESPONSE; // surface the provider's 4xx, else generic
206 }
207 return st;
208}
209} // namespace
210
211int pc_oauth2_exchange_code(const char *token_url, const char *code, const char *redirect_uri, const char *client_id,
212 const char *client_secret, const char *code_verifier, pc_o_auth2_tokens *out)
213{
214 int n = pc_oauth2_build_code_request(code, redirect_uri, client_id, client_secret, code_verifier, s_oauth.body,
215 sizeof(s_oauth.body));
216 return post_and_parse(s_oauth, token_url, n, out);
217}
218
219int pc_oauth2_refresh(const char *token_url, const char *refresh_token, const char *client_id,
220 const char *client_secret, pc_o_auth2_tokens *out)
221{
222 int n =
223 pc_oauth2_build_refresh_request(refresh_token, client_id, client_secret, s_oauth.body, sizeof(s_oauth.body));
224 return post_and_parse(s_oauth, token_url, n, out);
225}
226
227#endif // PC_ENABLE_HTTP_CLIENT
228
229#endif // PC_ENABLE_OAUTH2
Base-16 conversion between raw bytes and their ASCII digits.
char pc_hex_digit(uint8_t nibble, bool upper=false)
Low nibble of nibble as a hex character, uppercase when upper.
Definition hex.h:30
Zero-heap outbound HTTP(S) client over raw lwIP (PC_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:577
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:623
Layer 6 (Presentation) - zero-heap JSON: a bounded writer and top-level reader.
OAuth2 token-endpoint client - authorization-code + refresh (PC_ENABLE_OAUTH2).