feat(auth): automatically generate anonymous user session
arcad/edge/pipeline/head This commit looks good
Details
arcad/edge/pipeline/head This commit looks good
Details
ref arcad/edge-menu#86
This commit is contained in:
parent
17808d14c9
commit
8eb441daee
|
@ -22,6 +22,7 @@ import (
|
||||||
appModuleMemory "forge.cadoles.com/arcad/edge/pkg/module/app/memory"
|
appModuleMemory "forge.cadoles.com/arcad/edge/pkg/module/app/memory"
|
||||||
authModule "forge.cadoles.com/arcad/edge/pkg/module/auth"
|
authModule "forge.cadoles.com/arcad/edge/pkg/module/auth"
|
||||||
authHTTP "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
|
authHTTP "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
|
||||||
|
authModuleMiddleware "forge.cadoles.com/arcad/edge/pkg/module/auth/middleware"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module/blob"
|
"forge.cadoles.com/arcad/edge/pkg/module/blob"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module/fetch"
|
"forge.cadoles.com/arcad/edge/pkg/module/fetch"
|
||||||
|
@ -221,6 +222,9 @@ func runApp(ctx context.Context, path string, address string, storageFile string
|
||||||
}
|
}
|
||||||
|
|
||||||
router := chi.NewRouter()
|
router := chi.NewRouter()
|
||||||
|
router.Use(authModuleMiddleware.AnonymousUser(
|
||||||
|
jwa.HS256, key,
|
||||||
|
))
|
||||||
router.Use(middleware.Logger)
|
router.Use(middleware.Logger)
|
||||||
router.Use(middleware.Compress(5))
|
router.Use(middleware.Compress(5))
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module/auth"
|
"forge.cadoles.com/arcad/edge/pkg/module/auth"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd"
|
"forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/auth/jwt"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||||
|
@ -112,7 +113,7 @@ func (h *LocalHandler) handleForm(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
account.Claims[auth.ClaimIssuer] = "local"
|
account.Claims[auth.ClaimIssuer] = "local"
|
||||||
|
|
||||||
token, err := generateSignedToken(h.algo, h.key, account.Claims)
|
token, err := jwt.GenerateSignedToken(h.algo, h.key, account.Claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "could not generate signed token", logger.E(errors.WithStack(err)))
|
logger.Error(ctx, "could not generate signed token", logger.E(errors.WithStack(err)))
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
|
@ -30,7 +30,7 @@ func WithJWT(getKeySet GetKeySetFunc) OptionFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindToken(r *http.Request, getKeySet GetKeySetFunc) (jwt.Token, error) {
|
func FindRawToken(r *http.Request) (string, error) {
|
||||||
authorization := r.Header.Get("Authorization")
|
authorization := r.Header.Get("Authorization")
|
||||||
|
|
||||||
// Retrieve token from Authorization header
|
// Retrieve token from Authorization header
|
||||||
|
@ -44,7 +44,7 @@ func FindToken(r *http.Request, getKeySet GetKeySetFunc) (jwt.Token, error) {
|
||||||
if rawToken == "" {
|
if rawToken == "" {
|
||||||
cookie, err := r.Cookie(CookieName)
|
cookie, err := r.Cookie(CookieName)
|
||||||
if err != nil && !errors.Is(err, http.ErrNoCookie) {
|
if err != nil && !errors.Is(err, http.ErrNoCookie) {
|
||||||
return nil, errors.WithStack(err)
|
return "", errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cookie != nil {
|
if cookie != nil {
|
||||||
|
@ -53,7 +53,16 @@ func FindToken(r *http.Request, getKeySet GetKeySetFunc) (jwt.Token, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if rawToken == "" {
|
if rawToken == "" {
|
||||||
return nil, errors.WithStack(ErrUnauthenticated)
|
return "", errors.WithStack(ErrUnauthenticated)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindToken(r *http.Request, getKeySet GetKeySetFunc) (jwt.Token, error) {
|
||||||
|
rawToken, err := FindRawToken(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
keySet, err := getKeySet()
|
keySet, err := getKeySet()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package http
|
package jwt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
@ -9,7 +9,7 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func generateSignedToken(algo jwa.KeyAlgorithm, key jwk.Key, claims map[string]any) ([]byte, error) {
|
func GenerateSignedToken(algo jwa.KeyAlgorithm, key jwk.Key, claims map[string]any) ([]byte, error) {
|
||||||
token := jwt.New()
|
token := jwt.New()
|
||||||
|
|
||||||
if err := token.Set(jwt.NotBeforeKey, time.Now()); err != nil {
|
if err := token.Set(jwt.NotBeforeKey, time.Now()); err != nil {
|
|
@ -0,0 +1,113 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/auth"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/auth/jwt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||||
|
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const AnonIssuer = "anon"
|
||||||
|
|
||||||
|
func AnonymousUser(algo jwa.KeyAlgorithm, key jwk.Key, funcs ...AnonymousUserOptionFunc) func(next http.Handler) http.Handler {
|
||||||
|
opts := defaultAnonymousUserOptions()
|
||||||
|
for _, fn := range funcs {
|
||||||
|
fn(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
rawToken, err := auth.FindRawToken(r)
|
||||||
|
|
||||||
|
// If request already has a raw token, we do nothing
|
||||||
|
if rawToken != "" && err == nil {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
uuid, err := uuid.NewUUID()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not generate uuid for anonymous user", logger.E(errors.WithStack(err)))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subject := fmt.Sprintf("%s-%s", AnonIssuer, uuid.String())
|
||||||
|
preferredUsername, err := generateRandomPreferredUsername(8)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not generate preferred username for anonymous user", logger.E(errors.WithStack(err)))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := map[string]any{
|
||||||
|
auth.ClaimSubject: subject,
|
||||||
|
auth.ClaimIssuer: AnonIssuer,
|
||||||
|
auth.ClaimPreferredUsername: preferredUsername,
|
||||||
|
auth.ClaimEdgeRole: opts.Role,
|
||||||
|
auth.ClaimEdgeEntrypoint: opts.Entrypoint,
|
||||||
|
auth.ClaimEdgeTenant: opts.Tenant,
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := jwt.GenerateSignedToken(algo, key, claims)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not generate signed token", logger.E(errors.WithStack(err)))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cookieDomain, err := opts.GetCookieDomain(r)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not retrieve cookie domain", logger.E(errors.WithStack(err)))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cookie := http.Cookie{
|
||||||
|
Name: auth.CookieName,
|
||||||
|
Value: string(token),
|
||||||
|
Domain: cookieDomain,
|
||||||
|
HttpOnly: false,
|
||||||
|
Expires: time.Now().Add(opts.CookieDuration),
|
||||||
|
Path: "/",
|
||||||
|
}
|
||||||
|
|
||||||
|
http.SetCookie(w, &cookie)
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRandomPreferredUsername(size int) (string, error) {
|
||||||
|
var letters = []rune("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||||
|
max := big.NewInt(int64(len(letters)))
|
||||||
|
|
||||||
|
b := make([]rune, size)
|
||||||
|
for i := range b {
|
||||||
|
idx, err := rand.Int(rand.Reader, max)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
b[i] = letters[idx.Int64()]
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("Anon %s", string(b)), nil
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GetCookieDomainFunc func(r *http.Request) (string, error)
|
||||||
|
|
||||||
|
func defaultGetCookieDomain(r *http.Request) (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnonymousUserOptions struct {
|
||||||
|
GetCookieDomain GetCookieDomainFunc
|
||||||
|
CookieDuration time.Duration
|
||||||
|
Tenant string
|
||||||
|
Entrypoint string
|
||||||
|
Role string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnonymousUserOptionFunc func(*AnonymousUserOptions)
|
||||||
|
|
||||||
|
func defaultAnonymousUserOptions() *AnonymousUserOptions {
|
||||||
|
return &AnonymousUserOptions{
|
||||||
|
GetCookieDomain: defaultGetCookieDomain,
|
||||||
|
CookieDuration: 24 * time.Hour,
|
||||||
|
Tenant: "",
|
||||||
|
Entrypoint: "",
|
||||||
|
Role: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCookieOptions(getCookieDomain GetCookieDomainFunc, duration time.Duration) AnonymousUserOptionFunc {
|
||||||
|
return func(opts *AnonymousUserOptions) {
|
||||||
|
opts.GetCookieDomain = getCookieDomain
|
||||||
|
opts.CookieDuration = duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTenant(tenant string) AnonymousUserOptionFunc {
|
||||||
|
return func(opts *AnonymousUserOptions) {
|
||||||
|
opts.Tenant = tenant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithEntrypoint(entrypoint string) AnonymousUserOptionFunc {
|
||||||
|
return func(opts *AnonymousUserOptions) {
|
||||||
|
opts.Entrypoint = entrypoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithRole(role string) AnonymousUserOptionFunc {
|
||||||
|
return func(opts *AnonymousUserOptions) {
|
||||||
|
opts.Role = role
|
||||||
|
}
|
||||||
|
}
|
|
@ -92,7 +92,7 @@ var Edge=(()=>{var K3=Object.create;var Mi=Object.defineProperty,Y3=Object.defin
|
||||||
</edge-menu-item>
|
</edge-menu-item>
|
||||||
`}_canAccess(t){var a,o;let i=((a=this._profile)==null?void 0:a.edge_role)||"visitor",n=((o=t.metadata)==null?void 0:o.minimumRole)||"visitor";return sb[i]>=sb[n]}_renderProfile(){let t=this._profile;return re`
|
`}_canAccess(t){var a,o;let i=((a=this._profile)==null?void 0:a.edge_role)||"visitor",n=((o=t.metadata)==null?void 0:o.minimumRole)||"visitor";return sb[i]>=sb[n]}_renderProfile(){let t=this._profile;return re`
|
||||||
<edge-menu-item name='profile' label="${(t==null?void 0:t.preferred_username)||"Profile"}" icon-url='${Zm}'>
|
<edge-menu-item name='profile' label="${(t==null?void 0:t.preferred_username)||"Profile"}" icon-url='${Zm}'>
|
||||||
${t?re`<edge-menu-sub-item name='login' label='Logout' icon-url='${nb}' link-url='/edge/auth/logout'></edge-menu-sub-item>`:re`<edge-menu-sub-item name='login' label='Login' icon-url='${tb}' link-url='/edge/auth/login'></edge-menu-sub-item>`}
|
${t&&t.iss!="anon"?re`<edge-menu-sub-item name='login' label='Logout' icon-url='${nb}' link-url='/edge/auth/logout'></edge-menu-sub-item>`:re`<edge-menu-sub-item name='login' label='Login' icon-url='${tb}' link-url='/edge/auth/login'></edge-menu-sub-item>`}
|
||||||
</edge-menu-item>
|
</edge-menu-item>
|
||||||
`}_handleMenuItemSelected(t){let i=t.detail.element;i.classList.add("selected"),i.classList.remove("unselected");for(let n,a=0;n=this._menuItems[a];a++)n!==i&&(n.unselect(),n.classList.add("unselected"))}_handleMenuItemUnselected(t){if(t.detail.element.classList.remove("selected"),this.renderRoot.querySelectorAll("edge-menu-item.selected").length===0)for(let a,o=0;a=this._menuItems[o];o++)a.classList.remove("unselected")}};le.styles=Ti`
|
`}_handleMenuItemSelected(t){let i=t.detail.element;i.classList.add("selected"),i.classList.remove("unselected");for(let n,a=0;n=this._menuItems[a];a++)n!==i&&(n.unselect(),n.classList.add("unselected"))}_handleMenuItemUnselected(t){if(t.detail.element.classList.remove("selected"),this.renderRoot.querySelectorAll("edge-menu-item.selected").length===0)for(let a,o=0;a=this._menuItems[o];o++)a.classList.remove("unselected")}};le.styles=Ti`
|
||||||
:host {
|
:host {
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -191,7 +191,7 @@ export class Menu extends LitElement {
|
||||||
return html`
|
return html`
|
||||||
<edge-menu-item name='profile' label="${profile?.preferred_username || 'Profile'}" icon-url='${UserCircleIcon}'>
|
<edge-menu-item name='profile' label="${profile?.preferred_username || 'Profile'}" icon-url='${UserCircleIcon}'>
|
||||||
${
|
${
|
||||||
profile ?
|
profile && profile.iss != "anon" ?
|
||||||
html`<edge-menu-sub-item name='login' label='Logout' icon-url='${LogoutIcon}' link-url='/edge/auth/logout'></edge-menu-sub-item>` :
|
html`<edge-menu-sub-item name='login' label='Logout' icon-url='${LogoutIcon}' link-url='/edge/auth/logout'></edge-menu-sub-item>` :
|
||||||
html`<edge-menu-sub-item name='login' label='Login' icon-url='${LoginIcon}' link-url='/edge/auth/login'></edge-menu-sub-item>`
|
html`<edge-menu-sub-item name='login' label='Login' icon-url='${LoginIcon}' link-url='/edge/auth/login'></edge-menu-sub-item>`
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["ES2015", "DOM"],
|
"lib": ["ES2015", "DOM"],
|
||||||
"experimentalDecorators": true
|
"experimentalDecorators": true,
|
||||||
|
"esModuleInterop": true
|
||||||
},
|
},
|
||||||
"include": ["pkg/sdk/client/src/index.d.ts", "pkg/sdk/client/src/**/*.ts", "pkg/sdk/client/src/**/*.svg"]
|
"include": ["pkg/sdk/client/src/index.d.ts", "pkg/sdk/client/src/**/*.ts", "pkg/sdk/client/src/**/*.svg"]
|
||||||
}
|
}
|
Loading…
Reference in New Issue