mirror of
https://github.com/1f349/lavender.git
synced 2024-12-22 07:34:06 +00:00
Exchange token and fetch userinfo
This commit is contained in:
parent
1f2be68470
commit
9228f6649e
@ -24,6 +24,7 @@ type UserAuth struct {
|
|||||||
type SessionData struct {
|
type SessionData struct {
|
||||||
ID string
|
ID string
|
||||||
DisplayName string
|
DisplayName string
|
||||||
|
UserInfo map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u UserAuth) IsGuest() bool {
|
func (u UserAuth) IsGuest() bool {
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"github.com/1f349/lavender/pages"
|
"github.com/1f349/lavender/pages"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
@ -11,7 +17,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h *HttpServer) loginGet(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) {
|
func (h *HttpServer) loginGet(rw http.ResponseWriter, req *http.Request, _ httprouter.Params, auth UserAuth) {
|
||||||
|
if !auth.IsGuest() {
|
||||||
|
h.SafeRedirect(rw, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cookie, err := req.Cookie("lavender-login-name")
|
cookie, err := req.Cookie("lavender-login-name")
|
||||||
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{
|
||||||
@ -27,7 +38,12 @@ func (h *HttpServer) loginGet(rw http.ResponseWriter, req *http.Request, _ httpr
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HttpServer) loginPost(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) {
|
func (h *HttpServer) loginPost(rw http.ResponseWriter, req *http.Request, _ httprouter.Params, auth UserAuth) {
|
||||||
|
if !auth.IsGuest() {
|
||||||
|
h.SafeRedirect(rw, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if req.PostFormValue("not-you") == "1" {
|
if req.PostFormValue("not-you") == "1" {
|
||||||
http.SetCookie(rw, &http.Cookie{
|
http.SetCookie(rw, &http.Cookie{
|
||||||
Name: "lavender-login-name",
|
Name: "lavender-login-name",
|
||||||
@ -77,3 +93,79 @@ func (h *HttpServer) loginPost(rw http.ResponseWriter, req *http.Request, _ http
|
|||||||
nextUrl := oa2conf.AuthCodeURL(state, oauth2.SetAuthURLParam("login_name", loginUn))
|
nextUrl := oa2conf.AuthCodeURL(state, oauth2.SetAuthURLParam("login_name", loginUn))
|
||||||
http.Redirect(rw, req, nextUrl, http.StatusFound)
|
http.Redirect(rw, req, nextUrl, http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) loginCallback(rw http.ResponseWriter, req *http.Request, _ httprouter.Params, auth UserAuth) {
|
||||||
|
flowState, ok := h.flowState.Get(req.FormValue("state"))
|
||||||
|
if !ok {
|
||||||
|
http.Error(rw, "Invalid flow state", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token, err := flowState.sso.OAuth2Config.Exchange(context.Background(), req.FormValue("code"), oauth2.SetAuthURLParam("redirect_uri", h.conf.BaseUrl+"/callback"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, "Failed to exchange code for token", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := flowState.sso.OAuth2Config.Client(context.Background(), token).Get(flowState.sso.UserInfoEndpoint)
|
||||||
|
if err != nil || res.StatusCode != 200 {
|
||||||
|
rw.WriteHeader(http.StatusInternalServerError)
|
||||||
|
if err != nil {
|
||||||
|
_, _ = rw.Write([]byte(err.Error()))
|
||||||
|
} else {
|
||||||
|
_, _ = rw.Write([]byte(res.Status))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
var userInfoJson map[string]any
|
||||||
|
if err := json.NewDecoder(res.Body).Decode(&userInfoJson); err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
subject, ok := userInfoJson["sub"].(string)
|
||||||
|
if !ok {
|
||||||
|
http.Error(rw, "Invalid subject", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
subject += "@" + flowState.sso.Config.Namespace
|
||||||
|
|
||||||
|
displayName, ok := userInfoJson["name"].(string)
|
||||||
|
if !ok {
|
||||||
|
displayName = "Unknown Name"
|
||||||
|
}
|
||||||
|
|
||||||
|
// only continues if the above tx succeeds
|
||||||
|
auth.Data = SessionData{
|
||||||
|
ID: subject,
|
||||||
|
DisplayName: displayName,
|
||||||
|
UserInfo: userInfoJson,
|
||||||
|
}
|
||||||
|
if auth.SaveSessionData() != nil {
|
||||||
|
http.Error(rw, "Failed to save session", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.setLoginDataCookie(rw, auth.Data.ID) {
|
||||||
|
http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.SafeRedirect(rw, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HttpServer) setLoginDataCookie(rw http.ResponseWriter, userId string) bool {
|
||||||
|
encryptedData, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, h.signingKey.PublicKey(), []byte(userId), []byte("login-data"))
|
||||||
|
if err != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
encryptedString := base64.RawStdEncoding.EncodeToString(encryptedData)
|
||||||
|
http.SetCookie(rw, &http.Cookie{
|
||||||
|
Name: "login-data",
|
||||||
|
Value: encryptedString,
|
||||||
|
Path: "/",
|
||||||
|
Expires: time.Now().AddDate(0, 3, 0),
|
||||||
|
Secure: true,
|
||||||
|
SameSite: http.SameSiteStrictMode,
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/go-oauth2/oauth2/v4/manage"
|
"github.com/go-oauth2/oauth2/v4/manage"
|
||||||
"github.com/go-oauth2/oauth2/v4/server"
|
"github.com/go-oauth2/oauth2/v4/server"
|
||||||
"github.com/go-oauth2/oauth2/v4/store"
|
"github.com/go-oauth2/oauth2/v4/store"
|
||||||
|
"github.com/go-session/session"
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -44,6 +45,8 @@ type flowStateData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Server {
|
func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Server {
|
||||||
|
session.InitManager(session.SetCookieName("lavender_session"))
|
||||||
|
|
||||||
r := httprouter.New()
|
r := httprouter.New()
|
||||||
|
|
||||||
// remove last slash from baseUrl
|
// remove last slash from baseUrl
|
||||||
@ -117,8 +120,9 @@ func NewHttpServer(conf Conf, db *database.DB, signingKey mjwt.Signer) *http.Ser
|
|||||||
r.GET("/", hs.OptionalAuthentication(hs.Home))
|
r.GET("/", hs.OptionalAuthentication(hs.Home))
|
||||||
|
|
||||||
// login
|
// login
|
||||||
r.GET("/login", hs.loginGet)
|
r.GET("/login", hs.OptionalAuthentication(hs.loginGet))
|
||||||
r.POST("/login", hs.loginPost)
|
r.POST("/login", hs.OptionalAuthentication(hs.loginPost))
|
||||||
|
r.GET("/callback", hs.OptionalAuthentication(hs.loginCallback))
|
||||||
r.POST("/logout", hs.RequireAuthentication(func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth) {
|
r.POST("/logout", hs.RequireAuthentication(func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth) {
|
||||||
lNonce, ok := auth.Session.Get("action-nonce")
|
lNonce, ok := auth.Session.Get("action-nonce")
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -2,11 +2,13 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Test Client</title>
|
<title>Test Client</title>
|
||||||
|
<script src="popup.js"></script>
|
||||||
<script>
|
<script>
|
||||||
let currentLoginPopup = null;
|
|
||||||
let currentTokens = null;
|
let currentTokens = null;
|
||||||
const ssoService = "http://localhost:9090";
|
const ssoService = "http://localhost:9090";
|
||||||
|
|
||||||
|
POP2.init(ssoService + "/authorize", "a", "openid profile", 500, 600);
|
||||||
|
|
||||||
function updateTokenInfo(data) {
|
function updateTokenInfo(data) {
|
||||||
currentTokens = data.tokens;
|
currentTokens = data.tokens;
|
||||||
data.tokens = {
|
data.tokens = {
|
||||||
@ -30,17 +32,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("message", function (event) {
|
|
||||||
if (event.origin !== ssoService) return;
|
|
||||||
if (isObject(event.data)) {
|
|
||||||
updateTokenInfo(event.data);
|
|
||||||
|
|
||||||
if (currentLoginPopup) currentLoginPopup.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
alert("Failed to log user in: the login data was probably corrupted");
|
|
||||||
});
|
|
||||||
|
|
||||||
function parseJwt(token) {
|
function parseJwt(token) {
|
||||||
const base64Url = token.split('.')[1];
|
const base64Url = token.split('.')[1];
|
||||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
@ -50,59 +41,19 @@
|
|||||||
return JSON.parse(jsonPayload);
|
return JSON.parse(jsonPayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isObject(obj) {
|
|
||||||
return obj != null && obj.constructor.name === "Object"
|
|
||||||
}
|
|
||||||
|
|
||||||
function popupCenterScreen(url, title, w, h, focus) {
|
|
||||||
const top = (screen.availHeight - h) / 4, left = (screen.availWidth - w) / 2;
|
|
||||||
const popup = openWindow(url, title, `scrollbars=yes,width=${w},height=${h},top=${top},left=${left}`);
|
|
||||||
if (focus === true && window.focus) popup.focus();
|
|
||||||
return popup;
|
|
||||||
}
|
|
||||||
|
|
||||||
function openWindow(url, winnm, options) {
|
|
||||||
var wTop = firstAvailableValue([window.screen.availTop, window.screenY, window.screenTop, 0]);
|
|
||||||
var wLeft = firstAvailableValue([window.screen.availLeft, window.screenX, window.screenLeft, 0]);
|
|
||||||
var top = 0, left = 0;
|
|
||||||
var result;
|
|
||||||
if ((result = /top=(\d+)/g.exec(options))) top = parseInt(result[1]);
|
|
||||||
if ((result = /left=(\d+)/g.exec(options))) left = parseInt(result[1]);
|
|
||||||
if (options) {
|
|
||||||
options = options.replace("top=" + top, "top=" + (parseInt(top) + wTop));
|
|
||||||
options = options.replace("left=" + left, "left=" + (parseInt(left) + wLeft));
|
|
||||||
w = window.open(url, winnm, options);
|
|
||||||
} else w = window.open(url, winnm);
|
|
||||||
return w;
|
|
||||||
}
|
|
||||||
|
|
||||||
function firstAvailableValue(arr) {
|
|
||||||
for (var i = 0; i < arr.length; i++)
|
|
||||||
if (typeof arr[i] != 'undefined')
|
|
||||||
return arr[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
function doThisThing() {
|
function doThisThing() {
|
||||||
if (currentLoginPopup) currentLoginPopup.close();
|
POP2.getToken(function (token) {
|
||||||
currentLoginPopup = popupCenterScreen(ssoService + '/popup?origin=' + encodeURIComponent(location.origin), 'Login with Lavender', 500, 500, false);
|
console.log(token);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshAllTokens() {
|
|
||||||
let req = await fetch(ssoService + '/refresh', {
|
|
||||||
method: 'POST',
|
|
||||||
mode: 'cors',
|
|
||||||
cache: 'no-cache',
|
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({"token": currentTokens.refresh}),
|
|
||||||
});
|
|
||||||
let reqJson = await req.json();
|
|
||||||
updateTokenInfo(reqJson);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
}
|
||||||
|
|
||||||
#someTextArea {
|
#someTextArea {
|
||||||
width: 400px;
|
width: 400px;
|
||||||
height: 400px;
|
height: 400px;
|
||||||
@ -121,7 +72,6 @@
|
|||||||
<main>
|
<main>
|
||||||
<div>
|
<div>
|
||||||
<button onclick="doThisThing();">Login</button>
|
<button onclick="doThisThing();">Login</button>
|
||||||
<button onclick="refreshAllTokens();">Refresh</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; gap: 2em;">
|
<div style="display:flex; gap: 2em;">
|
||||||
<div>
|
<div>
|
||||||
|
134
test-client/popup.js
Normal file
134
test-client/popup.js
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
/* Simple OAuth 2.0 Client flow library
|
||||||
|
|
||||||
|
Author: MrMelon54, timdream
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
POP2.init(client_id, scope)
|
||||||
|
Initialize the library.
|
||||||
|
redirect_uri is the current page (window.location.href).
|
||||||
|
This function should be put before Analytics so that the second click won't result a page view register.
|
||||||
|
POP2.getToken(callback)
|
||||||
|
Send access token to the callback function as the first argument.
|
||||||
|
If not logged in this triggers login popup and execute login after logged in.
|
||||||
|
Be sure to call this function in user-triggered event (such as click) to prevent popup blocker.
|
||||||
|
If not sure do use isLoggedIn() below to check first.
|
||||||
|
POP2.isLoggedIn()
|
||||||
|
boolean
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
(function (w) {
|
||||||
|
const windowName = 'pop2_oauth2_login_popup';
|
||||||
|
|
||||||
|
if (window.name === windowName) {
|
||||||
|
if (
|
||||||
|
window.opener &&
|
||||||
|
window.opener.POP2
|
||||||
|
) {
|
||||||
|
if (window.location.hash.indexOf('access_token') !== -1) {
|
||||||
|
window.opener.POP2.receiveToken(
|
||||||
|
window.location.hash.replace(/^.*access_token=([^&]+).*$/, '$1'),
|
||||||
|
parseInt(window.location.hash.replace(/^.*expires_in=([^&]+).*$/, '$1'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (window.location.search.indexOf('error=')) {
|
||||||
|
window.opener.POP2.receiveToken('ERROR');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function popupCenterScreen(url, title, w, h) {
|
||||||
|
const top = (screen.availHeight - h) / 4, left = (screen.availWidth - w) / 2;
|
||||||
|
return openWindow(url, title, `scrollbars=yes,width=${w},height=${h},top=${top},left=${left}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openWindow(url, winnm, options) {
|
||||||
|
const wTop = firstAvailableValue([window.screen.availTop, window.screenY, window.screenTop, 0]);
|
||||||
|
const wLeft = firstAvailableValue([window.screen.availLeft, window.screenX, window.screenLeft, 0]);
|
||||||
|
let top = "0",
|
||||||
|
left = "0",
|
||||||
|
result;
|
||||||
|
if ((result = /top=(\d+)/g.exec(options))) top = parseInt(result[1]);
|
||||||
|
if ((result = /left=(\d+)/g.exec(options))) left = parseInt(result[1]);
|
||||||
|
if (options) {
|
||||||
|
options = options.replace("top=" + top, "top=" + (parseInt(top) + wTop));
|
||||||
|
options = options.replace("left=" + left, "left=" + (parseInt(left) + wLeft));
|
||||||
|
w = window.open(url, winnm, options);
|
||||||
|
} else w = window.open(url, winnm);
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstAvailableValue(arr) {
|
||||||
|
for (let i = 0; i < arr.length; i++)
|
||||||
|
if (typeof arr[i] != 'undefined')
|
||||||
|
return arr[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
let client_endpoint,
|
||||||
|
client_id,
|
||||||
|
scope = '',
|
||||||
|
redirect_uri = window.location.href.substr(0, window.location.href.length - window.location.hash.length).replace(/#$/, ''),
|
||||||
|
access_token,
|
||||||
|
callbackWaitForToken,
|
||||||
|
w_width = 400,
|
||||||
|
w_height = 360;
|
||||||
|
|
||||||
|
w.POP2 = {
|
||||||
|
// init
|
||||||
|
init: function (f_client_endpoint, f_client_id, f_scope, width, height) {
|
||||||
|
if (!f_client_endpoint) return false;
|
||||||
|
if (!f_client_id) return false;
|
||||||
|
client_endpoint = f_client_endpoint;
|
||||||
|
client_id = f_client_id;
|
||||||
|
if (f_scope) scope = f_scope;
|
||||||
|
if (width) w_width = width;
|
||||||
|
if (height) w_height = height;
|
||||||
|
},
|
||||||
|
// receive token from popup
|
||||||
|
receiveToken: function (token, expires_in) {
|
||||||
|
if (token !== 'ERROR') {
|
||||||
|
access_token = token;
|
||||||
|
if (callbackWaitForToken) callbackWaitForToken(access_token);
|
||||||
|
setTimeout(
|
||||||
|
function () {
|
||||||
|
access_token = undefined;
|
||||||
|
},
|
||||||
|
expires_in * 1000
|
||||||
|
);
|
||||||
|
} else if (token === false) {
|
||||||
|
callbackWaitForToken = undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// boolean, indicate logged in or not
|
||||||
|
isLoggedIn: function () {
|
||||||
|
return !!access_token;
|
||||||
|
},
|
||||||
|
// pass the access token to callback
|
||||||
|
// if not logged in this triggers login popup;
|
||||||
|
// use isLoggedIn to check login first to prevent popup blocker
|
||||||
|
getToken: function (callback) {
|
||||||
|
if (!client_id || !redirect_uri || !scope) {
|
||||||
|
alert('You need init() first. Check the program flow.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!access_token) {
|
||||||
|
callbackWaitForToken = callback;
|
||||||
|
popupCenterScreen(
|
||||||
|
client_endpoint
|
||||||
|
+ '?response_type=token'
|
||||||
|
+ '&redirect_uri=' + encodeURIComponent(redirect_uri)
|
||||||
|
+ '&scope=' + encodeURIComponent(scope)
|
||||||
|
+ '&client_id=' + encodeURIComponent(client_id),
|
||||||
|
windowName,
|
||||||
|
w_width,
|
||||||
|
w_height
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return callback(access_token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})(this);
|
Loading…
Reference in New Issue
Block a user