Return to logged out with fetchUserInfo fails, updates to test client

This commit is contained in:
Melon 2024-02-09 15:25:56 +00:00
parent b47d4c8ad3
commit e1825ce1e8
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
10 changed files with 118 additions and 110 deletions

3
go.mod
View File

@ -14,6 +14,7 @@ require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/julienschmidt/httprouter v1.3.0 github.com/julienschmidt/httprouter v1.3.0
github.com/mattn/go-sqlite3 v1.14.22 github.com/mattn/go-sqlite3 v1.14.22
github.com/rs/cors v1.10.1
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.4
golang.org/x/oauth2 v0.16.0 golang.org/x/oauth2 v0.16.0
) )
@ -35,7 +36,7 @@ require (
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/rtred v0.1.2 // indirect github.com/tidwall/rtred v0.1.2 // indirect
github.com/tidwall/tinyqueue v0.1.1 // indirect github.com/tidwall/tinyqueue v0.1.1 // indirect
golang.org/x/net v0.20.0 // indirect golang.org/x/net v0.21.0 // indirect
google.golang.org/appengine v1.6.8 // indirect google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.32.0 // indirect google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

6
go.sum
View File

@ -100,6 +100,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo=
github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
@ -177,8 +179,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=

View File

@ -12,13 +12,11 @@
<div>Log in as: <span>{{.LoginName}}</span></div> <div>Log in as: <span>{{.LoginName}}</span></div>
<div> <div>
<form method="POST" action="/login"> <form method="POST" action="/login">
<input type="hidden" name="origin" value="{{.Origin}}"/>
<button type="submit" name="not-you" value="1">Not You?</button> <button type="submit" name="not-you" value="1">Not You?</button>
</form> </form>
</div> </div>
<div> <div>
<form method="POST" action="/login"> <form method="POST" action="/login">
<input type="hidden" name="origin" value="{{.Origin}}"/>
<input type="hidden" name="loginname" value="{{.LoginName}}"/> <input type="hidden" name="loginname" value="{{.LoginName}}"/>
<button type="submit">Continue</button> <button type="submit">Continue</button>
</form> </form>

View File

@ -10,7 +10,6 @@
</header> </header>
<main> <main>
<form method="POST" action="/login"> <form method="POST" action="/login">
<input type="hidden" name="origin" value="{{.Origin}}"/>
<div> <div>
<label for="field_loginname">Login Name:</label> <label for="field_loginname">Login Name:</label>
<input type="text" name="loginname" id="field_loginname" required/> <input type="text" name="loginname" id="field_loginname" required/>

View File

@ -67,8 +67,9 @@ func (h *HttpServer) OptionalAuthentication(next UserHandler) httprouter.Handle
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
return return
} }
if auth.IsGuest() && h.readLoginDataCookie(rw, req, &auth) { if auth.IsGuest() {
return // if this fails internally it just sees the user as logged out
h.readLoginDataCookie(rw, req, &auth)
} }
next(rw, req, params, auth) next(rw, req, params, auth)
} }

View File

@ -32,14 +32,12 @@ func (h *HttpServer) loginGet(rw http.ResponseWriter, req *http.Request, _ httpr
if err == nil && cookie.Valid() == nil { if err == nil && cookie.Valid() == nil {
pages.RenderPageTemplate(rw, "login-memory", map[string]any{ pages.RenderPageTemplate(rw, "login-memory", map[string]any{
"ServiceName": h.conf.ServiceName, "ServiceName": h.conf.ServiceName,
"Origin": req.URL.Query().Get("origin"),
"LoginName": cookie.Value, "LoginName": cookie.Value,
}) })
return return
} }
pages.RenderPageTemplate(rw, "login", map[string]any{ pages.RenderPageTemplate(rw, "login", map[string]any{
"ServiceName": h.conf.ServiceName, "ServiceName": h.conf.ServiceName,
"Origin": req.URL.Query().Get("origin"),
}) })
} }
@ -60,9 +58,6 @@ func (h *HttpServer) loginPost(rw http.ResponseWriter, req *http.Request, _ http
}) })
http.Redirect(rw, req, (&url.URL{ http.Redirect(rw, req, (&url.URL{
Path: "/login", Path: "/login",
RawQuery: url.Values{
"origin": []string{req.PostFormValue("origin")},
}.Encode(),
}).String(), http.StatusFound) }).String(), http.StatusFound)
return return
} }
@ -111,8 +106,9 @@ func (h *HttpServer) loginCallback(rw http.ResponseWriter, req *http.Request, _
return return
} }
sessionData, done := h.fetchUserInfo(rw, err, flowState.sso, token) sessionData := h.fetchUserInfo(rw, err, flowState.sso, token)
if !done { if sessionData.ID == "" {
http.Error(rw, "Failed to fetch user info", http.StatusInternalServerError)
return return
} }
@ -166,63 +162,57 @@ func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, userId string, t
return false return false
} }
func (h *HttpServer) readLoginDataCookie(rw http.ResponseWriter, req *http.Request, u *UserAuth) bool { func (h *HttpServer) readLoginDataCookie(rw http.ResponseWriter, req *http.Request, u *UserAuth) {
loginCookie, err := req.Cookie("lavender-login-data") loginCookie, err := req.Cookie("lavender-login-data")
if err != nil { if err != nil {
return false return
} }
decryptedBytes, err := base64.RawStdEncoding.DecodeString(loginCookie.Value) decryptedBytes, err := base64.RawStdEncoding.DecodeString(loginCookie.Value)
if err != nil { if err != nil {
return false return
} }
decryptedData, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, h.signingKey.PrivateKey(), decryptedBytes, []byte("lavender-login-data")) decryptedData, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, h.signingKey.PrivateKey(), decryptedBytes, []byte("lavender-login-data"))
if err != nil { if err != nil {
return false return
} }
buf := bytes.NewBuffer(decryptedData) buf := bytes.NewBuffer(decryptedData)
userId, err := buf.ReadString(0) userId, err := buf.ReadString(0)
if err != nil { if err != nil {
return false return
} }
userId = strings.TrimSuffix(userId, "\x00") userId = strings.TrimSuffix(userId, "\x00")
var token *oauth2.Token var token *oauth2.Token
err = json.NewDecoder(buf).Decode(&token) err = json.NewDecoder(buf).Decode(&token)
if err != nil { if err != nil {
return false return
} }
sso := h.manager.FindServiceFromLogin(userId) sso := h.manager.FindServiceFromLogin(userId)
if sso == nil { if sso == nil {
return false return
} }
sessionData, done := h.fetchUserInfo(rw, err, sso, token) u.Data = h.fetchUserInfo(rw, err, sso, token)
if !done {
return true
}
u.Data = sessionData
return false
} }
func (h *HttpServer) fetchUserInfo(rw http.ResponseWriter, err error, sso *issuer.WellKnownOIDC, token *oauth2.Token) (SessionData, bool) { func (h *HttpServer) fetchUserInfo(rw http.ResponseWriter, err error, sso *issuer.WellKnownOIDC, token *oauth2.Token) SessionData {
res, err := sso.OAuth2Config.Client(context.Background(), token).Get(sso.UserInfoEndpoint) res, err := sso.OAuth2Config.Client(context.Background(), token).Get(sso.UserInfoEndpoint)
if err != nil || res.StatusCode != http.StatusOK { if err != nil || res.StatusCode != http.StatusOK {
return SessionData{}, false return SessionData{}
} }
defer res.Body.Close() defer res.Body.Close()
var userInfoJson UserInfoFields var userInfoJson UserInfoFields
if err := json.NewDecoder(res.Body).Decode(&userInfoJson); err != nil { if err := json.NewDecoder(res.Body).Decode(&userInfoJson); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
return SessionData{}, false return SessionData{}
} }
subject, ok := userInfoJson.GetString("sub") subject, ok := userInfoJson.GetString("sub")
if !ok { if !ok {
http.Error(rw, "Invalid subject", http.StatusInternalServerError) http.Error(rw, "Invalid subject", http.StatusInternalServerError)
return SessionData{}, false return SessionData{}
} }
subject += "@" + sso.Config.Namespace subject += "@" + sso.Config.Namespace
@ -231,5 +221,5 @@ func (h *HttpServer) fetchUserInfo(rw http.ResponseWriter, err error, sso *issue
ID: subject, ID: subject,
DisplayName: displayName, DisplayName: displayName,
UserInfo: userInfoJson, UserInfo: userInfoJson,
}, true }
} }

View File

@ -169,7 +169,15 @@ func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Ser
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
} }
}) })
r.GET("/userinfo", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) { userInfoRequest := func(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) {
rw.Header().Set("Access-Control-Allow-Credentials", "true")
rw.Header().Set("Access-Control-Allow-Headers", "Authorization,Content-Type")
rw.Header().Set("Access-Control-Allow-Origin", strings.TrimSuffix(req.Referer(), "/"))
rw.Header().Set("Access-Control-Allow-Methods", "GET")
if req.Method == http.MethodOptions {
return
}
token, err := oauthSrv.ValidationBearerToken(req) token, err := oauthSrv.ValidationBearerToken(req)
if err != nil { if err != nil {
http.Error(rw, "403 Forbidden", http.StatusForbidden) http.Error(rw, "403 Forbidden", http.StatusForbidden)
@ -190,7 +198,9 @@ func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Ser
m["updated_at"] = time.Now().Unix() m["updated_at"] = time.Now().Unix()
_ = json.NewEncoder(rw).Encode(m) _ = json.NewEncoder(rw).Encode(m)
}) }
r.GET("/userinfo", userInfoRequest)
r.OPTIONS("/userinfo", userInfoRequest)
return &http.Server{ return &http.Server{
Addr: conf.Listen, Addr: conf.Listen,

View File

@ -1,92 +1,72 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>Test Client</title> <title>Test Client</title>
<script src="popup.js"></script> <script src="pop2.js"></script>
<script> <script>
let currentTokens = null; const ssoService = "http://localhost:9090";
const ssoService = "http://localhost:9090";
POP2.init(ssoService + "/authorize", "f4cdb93d-fe28-427b-b037-f03f44c86a16", "openid profile", 500, 600); POP2.init(ssoService + "/authorize", "f4cdb93d-fe28-427b-b037-f03f44c86a16", "openid profile", 500, 600);
function updateTokenInfo(data) { function updateTokenInfo(data) {
currentTokens = data.tokens; document.getElementById("someTextArea").textContent = JSON.stringify(data, null, 2);
data.tokens = { }
access: "*****",
refresh: "*****",
}
document.getElementById("someTextArea").textContent = JSON.stringify(data, null, 2);
let perms = document.getElementById("somePerms");
while (perms.childNodes.length > 0) {
perms.childNodes.item(0).remove();
}
document.getElementById("tokenValues").textContent = JSON.stringify(currentTokens, null, 2);
let jwt = parseJwt(currentTokens.access); function parseJwt(token) {
if (jwt.per != null) { const base64Url = token.split('.')[1];
jwt.per.forEach(function (x) { const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
let a = document.createElement("li"); const jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function (c) {
a.textContent = x; return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
perms.appendChild(a); }).join(''));
}); return JSON.parse(jsonPayload);
} }
}
function parseJwt(token) { function doThisThing() {
const base64Url = token.split('.')[1]; POP2.clientRequest(ssoService + "/userinfo", {}, true).then(function (x) {
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); console.log(x);
const jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function (c) { }).catch(function (x) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); console.error(x);
}).join('')); });
return JSON.parse(jsonPayload); }
} </script>
<style>
:root {
color-scheme: light dark;
}
function doThisThing() { #someTextArea {
POP2.getToken(function (token) { width: 400px;
console.log(token); height: 400px;
}); }
}
#tokenValues {
</script> width: 400px;
<style> height: 400px;
:root { }
color-scheme: light dark; </style>
}
#someTextArea {
width: 400px;
height: 400px;
}
#tokenValues {
width: 400px;
height: 400px;
}
</style>
</head> </head>
<body> <body>
<header> <header>
<h1>Test Client</h1> <h1>Test Client</h1>
</header> </header>
<main> <main>
<div>
<button onclick="doThisThing();">Login</button>
</div>
<div style="display:flex; gap: 2em;">
<div> <div>
<button onclick="doThisThing();">Login</button> <div>
<label for="someTextArea"></label><textarea id="someTextArea"></textarea>
</div>
<div>
<label for="tokenValues"></label><textarea id="tokenValues"></textarea>
</div>
</div> </div>
<div style="display:flex; gap: 2em;"> <div>
<div> <p>Permissions:</p>
<div> <ul id="somePerms"></ul>
<label for="someTextArea"></label><textarea id="someTextArea"></textarea>
</div>
<div>
<label for="tokenValues"></label><textarea id="tokenValues"></textarea>
</div>
</div>
<div>
<p>Permissions:</p>
<ul id="somePerms"></ul>
</div>
</div> </div>
</div>
</main> </main>
</body> </body>
</html> </html>

View File

@ -71,7 +71,7 @@
client_id, client_id,
scope = '', scope = '',
redirect_uri = window.location.href.substr(0, window.location.href.length - window.location.hash.length).replace(/#$/, ''), redirect_uri = window.location.href.substr(0, window.location.href.length - window.location.hash.length).replace(/#$/, ''),
access_token, access_token = localStorage.getItem("pop2_access_token"),
callbackWaitForToken, callbackWaitForToken,
w_width = 400, w_width = 400,
w_height = 360; w_height = 360;
@ -91,10 +91,12 @@
receiveToken: function (token, expires_in) { receiveToken: function (token, expires_in) {
if (token !== 'ERROR') { if (token !== 'ERROR') {
access_token = token; access_token = token;
localStorage.setItem("pop2_access_token", access_token);
if (callbackWaitForToken) callbackWaitForToken(access_token); if (callbackWaitForToken) callbackWaitForToken(access_token);
setTimeout( setTimeout(
function () { function () {
access_token = undefined; access_token = undefined;
localStorage.removeItem("pop2_access_token");
}, },
expires_in * 1000 expires_in * 1000
); );
@ -129,6 +131,31 @@
} else { } else {
return callback(access_token); return callback(access_token);
} }
},
clientRequest: function (resource, options, refresh = false) {
const sendRequest = function () {
options.credentials = 'include';
if (options.headers) {
options.headers['Authorization'] = 'Bearer ' + access_token;
}
return fetch(resource, options);
};
if (!refresh) return sendRequest();
else {
return new Promise(function (res, rej) {
sendRequest().then(function (x) {
res(x)
}).catch(function () {
w.POP2.getToken(function () {
sendRequest().then(function (x) {
res(x);
}).catch(function (x) {
rej(x);
});
})
});
});
}
} }
}; };
})(this); })(window);