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/julienschmidt/httprouter v1.3.0
github.com/mattn/go-sqlite3 v1.14.22
github.com/rs/cors v1.10.1
github.com/stretchr/testify v1.8.4
golang.org/x/oauth2 v0.16.0
)
@ -35,7 +36,7 @@ require (
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/rtred v0.1.2 // 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/protobuf v1.32.0 // 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/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
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/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
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-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.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
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.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
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>
<form method="POST" action="/login">
<input type="hidden" name="origin" value="{{.Origin}}"/>
<button type="submit" name="not-you" value="1">Not You?</button>
</form>
</div>
<div>
<form method="POST" action="/login">
<input type="hidden" name="origin" value="{{.Origin}}"/>
<input type="hidden" name="loginname" value="{{.LoginName}}"/>
<button type="submit">Continue</button>
</form>

View File

@ -10,7 +10,6 @@
</header>
<main>
<form method="POST" action="/login">
<input type="hidden" name="origin" value="{{.Origin}}"/>
<div>
<label for="field_loginname">Login Name:</label>
<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)
return
}
if auth.IsGuest() && h.readLoginDataCookie(rw, req, &auth) {
return
if auth.IsGuest() {
// if this fails internally it just sees the user as logged out
h.readLoginDataCookie(rw, req, &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 {
pages.RenderPageTemplate(rw, "login-memory", map[string]any{
"ServiceName": h.conf.ServiceName,
"Origin": req.URL.Query().Get("origin"),
"LoginName": cookie.Value,
})
return
}
pages.RenderPageTemplate(rw, "login", map[string]any{
"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{
Path: "/login",
RawQuery: url.Values{
"origin": []string{req.PostFormValue("origin")},
}.Encode(),
}).String(), http.StatusFound)
return
}
@ -111,8 +106,9 @@ func (h *HttpServer) loginCallback(rw http.ResponseWriter, req *http.Request, _
return
}
sessionData, done := h.fetchUserInfo(rw, err, flowState.sso, token)
if !done {
sessionData := h.fetchUserInfo(rw, err, flowState.sso, token)
if sessionData.ID == "" {
http.Error(rw, "Failed to fetch user info", http.StatusInternalServerError)
return
}
@ -166,63 +162,57 @@ func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, userId string, t
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")
if err != nil {
return false
return
}
decryptedBytes, err := base64.RawStdEncoding.DecodeString(loginCookie.Value)
if err != nil {
return false
return
}
decryptedData, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, h.signingKey.PrivateKey(), decryptedBytes, []byte("lavender-login-data"))
if err != nil {
return false
return
}
buf := bytes.NewBuffer(decryptedData)
userId, err := buf.ReadString(0)
if err != nil {
return false
return
}
userId = strings.TrimSuffix(userId, "\x00")
var token *oauth2.Token
err = json.NewDecoder(buf).Decode(&token)
if err != nil {
return false
return
}
sso := h.manager.FindServiceFromLogin(userId)
if sso == nil {
return false
return
}
sessionData, done := h.fetchUserInfo(rw, err, sso, token)
if !done {
return true
}
u.Data = sessionData
return false
u.Data = h.fetchUserInfo(rw, err, sso, token)
}
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)
if err != nil || res.StatusCode != http.StatusOK {
return SessionData{}, false
return SessionData{}
}
defer res.Body.Close()
var userInfoJson UserInfoFields
if err := json.NewDecoder(res.Body).Decode(&userInfoJson); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return SessionData{}, false
return SessionData{}
}
subject, ok := userInfoJson.GetString("sub")
if !ok {
http.Error(rw, "Invalid subject", http.StatusInternalServerError)
return SessionData{}, false
return SessionData{}
}
subject += "@" + sso.Config.Namespace
@ -231,5 +221,5 @@ func (h *HttpServer) fetchUserInfo(rw http.ResponseWriter, err error, sso *issue
ID: subject,
DisplayName: displayName,
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)
}
})
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)
if err != nil {
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()
_ = json.NewEncoder(rw).Encode(m)
})
}
r.GET("/userinfo", userInfoRequest)
r.OPTIONS("/userinfo", userInfoRequest)
return &http.Server{
Addr: conf.Listen,

View File

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

View File

@ -71,7 +71,7 @@
client_id,
scope = '',
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,
w_width = 400,
w_height = 360;
@ -91,10 +91,12 @@
receiveToken: function (token, expires_in) {
if (token !== 'ERROR') {
access_token = token;
localStorage.setItem("pop2_access_token", access_token);
if (callbackWaitForToken) callbackWaitForToken(access_token);
setTimeout(
function () {
access_token = undefined;
localStorage.removeItem("pop2_access_token");
},
expires_in * 1000
);
@ -129,6 +131,31 @@
} else {
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);