Skip to content

Commit 08ccc97

Browse files
cgi-bin: Port Web Interface OAuth login to device authorization grant
Replace the single-session authorization-code (PKCE) flow in do_login()/finish_login() with the device authorization grant flow, following the pattern already used by ippeveprinter's OAuth support in libcups. Each browser session tracks its own device grant in a CUPS_DEVGRANT cookie (the device code, polling interval, etc. as JSON). A request to "/?LOGIN=Login": - with no CUPS_DEVGRANT cookie: requests a new device grant (cupsOAuthGetDeviceGrant()) and shows the user code and verification URL via a new oauth-login.tmpl template, with a meta-refresh timed to the server's requested polling interval. - with a pending CUPS_DEVGRANT cookie: makes a single, non-blocking cupsOAuthGetTokens() attempt. On success, stores the access token in CUPS_BEARER and clears the grant cookie. While still pending (authorization_pending/slow_down), the same page is shown again. On a hard failure, the grant is discarded so the next visit requests a fresh one. A new grant is only requested on a POST (the existing Login button, which is protected by the CSRF token), not on the GET requests used for polling, so a stray GET can't silently start a new authorization request against the OAuth server. do_logout() now also clears the CUPS_DEVGRANT cookie and calls cupsOAuthClearTokens() to clear the server-side token store, in addition to clearing CUPS_BEARER as before. Notes for reviewers: - The old code's CUPS_REFERER/CUPS_REFERRER-based return-to-referrer redirect is dropped; the mismatched env var/cookie names meant it never actually worked, and login now always redirects to "/". - cupsOAuthGetDeviceGrant() looks up the client ID under CUPS_OAUTH_REDIRECT_URI; a server previously configured only for the authorization-code Web Interface flow may need its client ID re-registered under that redirect URI. - Not tested against a live OAuth provider: a local environment TLS trust issue (unrelated to this change) prevented HTTPS connections to an IdP during development. The device-grant call pattern mirrors tools/cups-oauth.c's existing, working usage; manual verification against a live IdP before merging is recommended.
1 parent 8897c06 commit 08ccc97

3 files changed

Lines changed: 103 additions & 156 deletions

File tree

‎cgi-bin/home.c‎

Lines changed: 95 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ static void do_login(void);
2121
static void do_logout(void);
2222
static void do_redirect(const char *url);
2323
static void do_search(char *query);
24-
static void finish_login(void);
2524
static void show_error(const char *title, const char *message, const char *error);
2625

2726

@@ -58,8 +57,6 @@ main(void)
5857
do_login();
5958
else if (cgiGetSize("LOGOUT"))
6059
do_logout();
61-
else if (!cgiIsPOST() && (cgiGetSize("code") || cgiGetSize("error")))
62-
finish_login();
6360
else
6461
do_dashboard();
6562

@@ -88,27 +85,17 @@ do_dashboard(void)
8885

8986

9087
/*
91-
* 'do_login()' - Redirect to the OAuth server's authorization endpoint.
88+
* 'do_login()' - Start or continue the OAuth device authorization flow.
9289
*/
9390

9491
static void
9592
do_login(void)
9693
{
97-
const char *oauth_uri = getenv("CUPS_OAUTH_SERVER"),
98-
// OAuth authorization server URL
99-
*referer = getenv("HTTP_REFERER"),
100-
// Referer: header
101-
*server_name = getenv("SERVER_NAME"),
102-
// SERVER_NAME value
103-
*server_port = getenv("SERVER_PORT"),
104-
// SERVER_PORT value
105-
*state = NULL; // State string
106-
char *client_id = NULL, // Client ID value
107-
*code_verifier = NULL, // Code verifier string
108-
*nonce = NULL, // Nonce string
109-
redirect_uri[1024], // redirect_uri value
110-
*url = NULL; // Authorization URL
111-
cups_json_t *metadata = NULL; // OAuth metadata
94+
const char *oauth_uri = getenv("CUPS_OAUTH_SERVER");
95+
// OAuth authorization server URL
96+
cups_json_t *metadata = NULL; // OAuth metadata
97+
const char *devgrant_cookie; // CUPS_DEVGRANT cookie value, if any
98+
cups_json_t *devgrant = NULL; // Device grant JSON, if any
11299

113100

114101
fputs("DEBUG2: do_login()\n", stderr);
@@ -120,58 +107,102 @@ do_login(void)
120107
goto done;
121108
}
122109

123-
fprintf(stderr, "DEBUG2: do_login: oauth_uri=\"%s\"\n", oauth_uri);
110+
// See if we have a pending device grant...
111+
if ((devgrant_cookie = cgiGetCookie("CUPS_DEVGRANT")) != NULL && devgrant_cookie[0])
112+
devgrant = cupsJSONImportString(devgrant_cookie);
124113

125-
// Get the redirect URL...
126-
if (!strcmp(server_name, "localhost"))
127-
snprintf(redirect_uri, sizeof(redirect_uri), "http://127.0.0.1:%s/", server_port);
128-
else
129-
snprintf(redirect_uri, sizeof(redirect_uri), "%s://%s:%s/", getenv("HTTPS") ? "https" : "http", server_name, server_port);
114+
if (devgrant)
115+
{
116+
const char *device_code; // Device code
130117

131-
fprintf(stderr, "DEBUG2: do_login: redirect_uri=\"%s\"\n", redirect_uri);
118+
if ((device_code = cupsJSONGetString(cupsJSONFind(devgrant, CUPS_ODEVGRANT_DEVICE_CODE))) != NULL)
119+
{
120+
char *bearer; // Access token
121+
time_t access_expires; // Expiration date
122+
123+
bearer = cupsOAuthGetTokens(oauth_uri, metadata, NULL, device_code, CUPS_OGRANT_DEVICE_CODE, NULL, &access_expires);
124+
125+
if (bearer)
126+
{
127+
// Got a token - save it, clear the grant cookie, and go home...
128+
cgiSetCookie("CUPS_DEVGRANT", "", NULL, NULL, time(NULL) - 1, 0);
129+
cgiSetCookie("CUPS_BEARER", bearer, NULL, NULL, access_expires, getenv("HTTPS") ? 1 : 0);
130+
131+
free(bearer);
132+
133+
do_redirect("/");
134+
goto done;
135+
}
136+
else if (access_expires == 0)
137+
{
138+
// Hard failure - discard this grant, fall through to request a new one...
139+
cupsJSONDelete(devgrant);
140+
devgrant = NULL;
141+
}
142+
// else: still pending ("authorization_pending"/"slow_down") - fall
143+
// through to show the same grant again...
144+
}
145+
else
146+
{
147+
// Malformed grant JSON - discard and fall through...
148+
cupsJSONDelete(devgrant);
149+
devgrant = NULL;
150+
}
151+
}
132152

133-
// Get the client ID...
134-
if ((client_id = cupsOAuthCopyClientId(oauth_uri, redirect_uri)) == NULL)
153+
if (!devgrant)
135154
{
136-
// Nothing saved, try to dynamically register one...
137-
if ((client_id = cupsOAuthGetClientId(oauth_uri, metadata, redirect_uri, /*logo_uri*/NULL, /*tos_uri*/NULL)) == NULL)
155+
char *temp; // JSON string
156+
157+
if (!cgiIsPOST())
138158
{
139-
// Nope, show an error...
140-
show_error(cgiText(_("OAuth Login")), cgiText(_("Unable to get authorization URL")), cgiText(_("No client ID configured for this server.")));
159+
// No pending grant, and this wasn't a POST (e.g. a stray GET to
160+
// "/?LOGIN=...") - don't silently start a new authorization request
161+
// against the OAuth server, just show the dashboard...
162+
do_dashboard();
141163
goto done;
142164
}
143-
}
144165

145-
fprintf(stderr, "DEBUG2: do_login: client_id=\"%s\"\n", client_id);
166+
// Request a new device grant...
167+
if ((devgrant = cupsOAuthGetDeviceGrant(oauth_uri, metadata, NULL, getenv("CUPS_OAUTH_SCOPES"))) == NULL)
168+
{
169+
show_error(cgiText(_("OAuth Login")), cgiText(_("Unable to get authorization URL")), cupsGetErrorString());
170+
goto done;
171+
}
146172

147-
// Make state and code verification strings...
148-
code_verifier = cupsOAuthMakeBase64Random(128);
149-
nonce = cupsOAuthMakeBase64Random(16);
150-
state = cgiGetCookie(CUPS_SID);
173+
if ((temp = cupsJSONExportString(devgrant)) != NULL)
174+
{
175+
cgiSetCookie("CUPS_DEVGRANT", temp, NULL, NULL, time(NULL) + (time_t)cupsJSONGetNumber(cupsJSONFind(devgrant, CUPS_ODEVGRANT_EXPIRES_IN)), 0);
176+
free(temp);
177+
}
178+
}
151179

152-
// Get the authorization URL
153-
if ((url = cupsOAuthMakeAuthorizationURL(oauth_uri, metadata, /*resource_uri*/NULL, getenv("CUPS_OAUTH_SCOPES"), client_id, code_verifier, nonce, redirect_uri, state)) == NULL)
180+
// Show the authorization page...
154181
{
155-
show_error(cgiText(_("OAuth Login")), cgiText(_("Unable to get authorization URL")), cupsGetErrorString());
156-
goto done;
157-
}
182+
const char *user_code = cupsJSONGetString(cupsJSONFind(devgrant, CUPS_ODEVGRANT_USER_CODE));
183+
const char *verification_uri = cupsJSONGetString(cupsJSONFind(devgrant, CUPS_ODEVGRANT_VERIFICATION_URI));
184+
double interval = cupsJSONGetNumber(cupsJSONFind(devgrant, CUPS_ODEVGRANT_INTERVAL));
185+
char refresh[32]; // Refresh value
186+
187+
snprintf(refresh, sizeof(refresh), "%d;URL=/?LOGIN=Login", interval > 0.0 ? (int)interval : 5);
188+
cgiSetVariable("REFRESH_PAGE", refresh);
158189

159-
// Redirect...
160-
cgiSetCookie("CUPS_OAUTH_STATE", state, /*path*/NULL, /*domain*/NULL, time(NULL) + 300, /*secure*/0);
190+
cgiSetVariable("USER_CODE", user_code ? user_code : "");
161191

162-
if (referer)
163-
cgiSetCookie("CUPS_REFERRER", referer, /*path*/NULL, /*domain*/NULL, time(NULL) + 300, /*secure*/0);
192+
if (verification_uri && (!strncmp(verification_uri, "http://", 7) || !strncmp(verification_uri, "https://", 8)))
193+
cgiSetVariable("VERIFICATION_URI", verification_uri);
194+
else
195+
cgiSetVariable("VERIFICATION_URI", "");
164196

165-
do_redirect(url);
197+
cgiStartHTML(cgiText(_("Authorize Access")));
198+
cgiCopyTemplateLang("oauth-login.tmpl");
199+
cgiEndHTML();
200+
}
166201

167202
done:
168203

169-
// Free memory...
170-
free(client_id);
171-
free(code_verifier);
204+
cupsJSONDelete(devgrant);
172205
cupsJSONDelete(metadata);
173-
free(nonce);
174-
free(url);
175206
}
176207

177208

@@ -182,10 +213,19 @@ do_login(void)
182213
static void
183214
do_logout(void)
184215
{
216+
const char *oauth_uri = getenv("CUPS_OAUTH_SERVER");
217+
// OAuth authorization server URL
218+
185219
// Clear the CUPS_BEARER cookie...
186-
cgiSetCookie("CUPS_BEARER", "", /*path*/NULL, /*domain*/NULL, time(NULL) - 1, /*secure*/0);
220+
cgiSetCookie("CUPS_BEARER", "", NULL, NULL, time(NULL) - 1, 0);
187221

188-
// Redirect back to the referrer...
222+
// Clear any pending device grant cookie...
223+
cgiSetCookie("CUPS_DEVGRANT", "", NULL, NULL, time(NULL) - 1, 0);
224+
225+
// Clear the stored OAuth tokens server-side as well...
226+
cupsOAuthClearTokens(oauth_uri, NULL);
227+
228+
// Redirect back to the dashboard...
189229
do_redirect("/");
190230
}
191231

@@ -231,107 +271,6 @@ do_search(char *query) /* I - Search string */
231271
}
232272

233273

234-
//
235-
// 'finish_login()' - Finish OAuth login and then redirect back to the original page.
236-
//
237-
238-
static void
239-
finish_login(void)
240-
{
241-
const char *oauth_uri = getenv("CUPS_OAUTH_SERVER"),
242-
// OAuth authorization server URL
243-
*referer = getenv("CUPS_REFERER"),
244-
// Referring URL
245-
*server_name = getenv("SERVER_NAME"),
246-
// SERVER_NAME value
247-
*server_port = getenv("SERVER_PORT");
248-
// SERVER_PORT value
249-
char *bearer = NULL, // Bearer token
250-
*client_id = NULL, // Client ID value
251-
*error, // Error string
252-
redirect_uri[1024]; // redirect_uri value
253-
const char *code; // Authorization code
254-
cups_json_t *metadata = NULL; // OAuth metadata
255-
time_t access_expires; // When the bearer token expires
256-
char scheme[32], // Referer scheme
257-
userpass[256], // Referer username:password
258-
host[256], // Referer host
259-
resource[1024]; // Referer resource
260-
int port; // Referer port
261-
262-
263-
// Show any error from authorization...
264-
if ((error = cgiGetVariable("error_description")) == NULL)
265-
error = cgiGetVariable("error");
266-
267-
if (error)
268-
{
269-
show_error(cgiText(_("OAuth Login")), cgiText(_("Unable to authorize access")), error);
270-
return;
271-
}
272-
273-
// Get the metadata...
274-
if ((metadata = cupsOAuthGetMetadata(oauth_uri)) == NULL)
275-
{
276-
show_error(cgiText(_("OAuth Login")), cgiText(_("Unable to get authorization server information")), cupsGetErrorString());
277-
goto done;
278-
}
279-
280-
// Get the redirect URL...
281-
if (!server_name || !strcmp(server_name, "localhost"))
282-
snprintf(redirect_uri, sizeof(redirect_uri), "http://127.0.0.1%s%s/", server_port ? ":" : "", server_port ? server_port : "");
283-
else
284-
snprintf(redirect_uri, sizeof(redirect_uri), "%s://%s%s%s/", getenv("HTTPS") ? "https" : "http", server_name, server_port ? ":" : "", server_port ? server_port : "");
285-
286-
fprintf(stderr, "DEBUG2: finish_login: redirect_uri=\"%s\"\n", redirect_uri);
287-
288-
// Get the client ID...
289-
if ((client_id = cupsOAuthCopyClientId(oauth_uri, redirect_uri)) == NULL)
290-
{
291-
// Nope, show an error...
292-
show_error(cgiText(_("OAuth Login")), cgiText(_("Unable to authorize access")), cgiText(_("No client ID configured for this server.")));
293-
goto done;
294-
}
295-
296-
fprintf(stderr, "DEBUG2: finish_login: client_id=\"%s\"\n", client_id);
297-
298-
// Get the code string...
299-
code = cgiGetVariable("code");
300-
301-
// Get the access token...
302-
if ((bearer = cupsOAuthGetTokens(oauth_uri, metadata, /*resource_uri*/NULL, code, CUPS_OGRANT_AUTHORIZATION_CODE, redirect_uri, &access_expires)) == NULL)
303-
{
304-
show_error(cgiText(_("OAuth Login")), cgiText(_("Unable to authorize access")), cupsGetErrorString());
305-
goto done;
306-
}
307-
308-
fprintf(stderr, "DEBUG2: finish_login: access_token=\"%s\", access_expires=%ld\n", bearer, (long)access_expires);
309-
310-
// Save it as a cookie...
311-
cgiSetCookie("CUPS_BEARER", bearer, /*path*/NULL, /*domain*/NULL, access_expires, /*secure*/0);
312-
313-
// Redirect...
314-
if (referer && server_name && server_port)
315-
{
316-
// Validate refererring URL value - must be http: or https:, use the server
317-
// name or localhost addresses, and use the same port...
318-
if (httpSeparateURI(HTTP_URI_CODING_ALL, referer, scheme, sizeof(scheme), userpass, sizeof(userpass), host, sizeof(host), &port, resource, sizeof(resource)) < HTTP_URI_STATUS_OK || (strcmp(scheme, "http") && strcmp(scheme, "https")) || (strcasecmp(host, server_name) && strcmp(host, "127.0.0.1") && strcmp(host, "[::1]")) || port != atoi(server_port))
319-
referer = NULL;
320-
}
321-
322-
do_redirect(referer ? referer : "/");
323-
324-
fputs("DEBUG2: finish_login: After redirect.\n", stderr);
325-
326-
done:
327-
328-
// Free memory...
329-
free(bearer);
330-
free(client_id);
331-
cupsJSONDelete(metadata);
332-
}
333-
334-
335274
//
336275
// 'show_error()' - Show an error message.
337276
//

‎templates/Makefile‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ FILES = \
5151
modify-class.tmpl \
5252
modify-printer.tmpl \
5353
norestart.tmpl \
54+
oauth-login.tmpl \
5455
option-boolean.tmpl \
5556
option-conflict.tmpl \
5657
option-header.tmpl \

‎templates/oauth-login.tmpl‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<H1 CLASS="title">{?title}</H1>
2+
3+
<P>This printer requires authorization for access.</P>
4+
5+
<P>Copy the following code: <STRONG>{?USER_CODE}</STRONG></P>
6+
7+
{?VERIFICATION_URI?<P>Then go to the following URL to authorize access: <A HREF="{VERIFICATION_URI}" TARGET="_blank">{VERIFICATION_URI}</A></P>:<P>Unable to display the authorization URL.</P>}

0 commit comments

Comments
 (0)