Collect Android browser sign-ins after the app was stopped
The Android app kept a pending browser sign-in in a thread inside the login screen. Android often kills the process while the browser is in front, so the approval reached the server but nothing collected it, and the app came back signed out with nothing saved. The pending device code is now kept in Prefs and collected whenever the login screen resumes. The app passes return_url=keeporg://device-approved when it starts the sign-in, and the server's approval page, after a password or single sign-on, links back to it. Only the keeporg: scheme is accepted. Against a server older than 0.9, which rejects the field, the app retries without it. Verified on an Android 15 emulator: approval collected after the process was killed, and the fallback against the 0.8 server. Server 0.9.0, Android 0.3.0 (versionCode 3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ef6598fa15
commit
318bd68908
+22
-2
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -187,15 +188,22 @@ func (s *Server) handleTOTPConfirm(w http.ResponseWriter, r *http.Request, u *st
|
||||
|
||||
func (s *Server) handleDeviceStart(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Label string `json:"label"`
|
||||
Label string `json:"label"`
|
||||
ReturnURL string `json:"return_url"`
|
||||
}
|
||||
if r.ContentLength > 0 && !readJSON(w, r, &req) {
|
||||
return
|
||||
}
|
||||
if req.ReturnURL != "" && !validReturnURL(req.ReturnURL) {
|
||||
fail(w, http.StatusBadRequest, "bad_request",
|
||||
"return_url must use the "+returnScheme+": scheme")
|
||||
return
|
||||
}
|
||||
d := &store.DeviceAuth{
|
||||
DeviceCode: secret.Token(),
|
||||
UserCode: secret.UserCode(),
|
||||
Label: req.Label,
|
||||
ReturnURL: req.ReturnURL,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(deviceTTL),
|
||||
}
|
||||
@@ -276,11 +284,13 @@ func (s *Server) handleDeviceApprove(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
code := strings.ToUpper(strings.TrimSpace(r.FormValue("code")))
|
||||
var returnURL string
|
||||
err := s.st.Update(func(st *store.State) error {
|
||||
d := st.DeviceByUserCode(code)
|
||||
if d == nil || time.Now().After(d.ExpiresAt) {
|
||||
return errors.New("unknown or expired code")
|
||||
}
|
||||
returnURL = d.ReturnURL
|
||||
u, err := checkPassword(st, r.FormValue("username"), r.FormValue("password"), r.FormValue("totp"))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -298,10 +308,20 @@ func (s *Server) handleDeviceApprove(w http.ResponseWriter, r *http.Request) {
|
||||
case err != nil:
|
||||
s.renderDevice(w, code, err.Error())
|
||||
default:
|
||||
s.renderDone(w, "Device approved", "You can close this page and return to the application.")
|
||||
s.renderApproved(w, returnURL)
|
||||
}
|
||||
}
|
||||
|
||||
// returnScheme is the only scheme a device may ask to be sent back to. It
|
||||
// names KeepOrg's own clients, so the approval page cannot be turned into a
|
||||
// redirect to anywhere else.
|
||||
const returnScheme = "keeporg"
|
||||
|
||||
func validReturnURL(raw string) bool {
|
||||
u, err := url.Parse(raw)
|
||||
return err == nil && u.Scheme == returnScheme && u.User == nil
|
||||
}
|
||||
|
||||
// changePassword replaces a user's password after proving the current one.
|
||||
// It is the one place a password changes, so the rules live here: the
|
||||
// current password is required even though the caller already holds a
|
||||
|
||||
Reference in New Issue
Block a user