authkit initial
This commit is contained in:
parent
5173b0a43d
commit
134393fbca
43 changed files with 5188 additions and 1 deletions
154
service_session.go
Normal file
154
service_session.go
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
package authkit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/errx"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// IssueSession mints an opaque session ID, persists the session record, and
|
||||
// returns the plaintext (for the cookie) plus the stored Session.
|
||||
func (a *Auth) IssueSession(ctx context.Context, userID uuid.UUID, userAgent string, ip netip.Addr) (string, *Session, error) {
|
||||
const op = "authkit.Auth.IssueSession"
|
||||
plaintext, hash, err := mintSecret(prefixSession, a.cfg.Random)
|
||||
if err != nil {
|
||||
return "", nil, errx.Wrap(op, err)
|
||||
}
|
||||
now := a.now()
|
||||
expires := now.Add(a.cfg.SessionIdleTTL)
|
||||
if cap := now.Add(a.cfg.SessionAbsoluteTTL); expires.After(cap) {
|
||||
expires = cap
|
||||
}
|
||||
s := &Session{
|
||||
IDHash: hash,
|
||||
UserID: userID,
|
||||
UserAgent: userAgent,
|
||||
IP: ip,
|
||||
CreatedAt: now,
|
||||
LastSeenAt: now,
|
||||
ExpiresAt: expires,
|
||||
}
|
||||
if err := a.deps.Sessions.CreateSession(ctx, s); err != nil {
|
||||
return "", nil, errx.Wrap(op, err)
|
||||
}
|
||||
return plaintext, s, nil
|
||||
}
|
||||
|
||||
// AuthenticateSession validates an opaque session string, slides the TTL,
|
||||
// resolves the user's roles+permissions, and returns a Principal. Expired or
|
||||
// unknown sessions return ErrSessionInvalid.
|
||||
func (a *Auth) AuthenticateSession(ctx context.Context, plaintext string) (*Principal, error) {
|
||||
const op = "authkit.Auth.AuthenticateSession"
|
||||
hash, ok := parseSecret(prefixSession, plaintext)
|
||||
if !ok {
|
||||
return nil, errx.Wrap(op, ErrSessionInvalid)
|
||||
}
|
||||
s, err := a.deps.Sessions.GetSession(ctx, hash)
|
||||
if err != nil {
|
||||
return nil, errx.Wrap(op, err)
|
||||
}
|
||||
now := a.now()
|
||||
if !s.ExpiresAt.After(now) {
|
||||
_ = a.deps.Sessions.DeleteSession(ctx, hash)
|
||||
return nil, errx.Wrap(op, ErrSessionInvalid)
|
||||
}
|
||||
|
||||
// Slide the idle TTL, capped at created_at + AbsoluteTTL so an active
|
||||
// session still expires at the absolute boundary.
|
||||
newExpires := now.Add(a.cfg.SessionIdleTTL)
|
||||
if cap := s.CreatedAt.Add(a.cfg.SessionAbsoluteTTL); newExpires.After(cap) {
|
||||
newExpires = cap
|
||||
}
|
||||
if err := a.deps.Sessions.TouchSession(ctx, hash, now, newExpires); err != nil {
|
||||
return nil, errx.Wrap(op, err)
|
||||
}
|
||||
|
||||
roles, perms, err := a.resolveRolesAndPermissions(ctx, s.UserID)
|
||||
if err != nil {
|
||||
return nil, errx.Wrap(op, err)
|
||||
}
|
||||
return &Principal{
|
||||
UserID: s.UserID,
|
||||
Method: AuthMethodSession,
|
||||
SessionID: hash,
|
||||
Roles: roles,
|
||||
Permissions: perms,
|
||||
IssuedAt: s.CreatedAt,
|
||||
ExpiresAt: newExpires,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RevokeSession deletes a single session by its plaintext id. Idempotent:
|
||||
// missing sessions are not an error (logout twice should not 500).
|
||||
func (a *Auth) RevokeSession(ctx context.Context, plaintext string) error {
|
||||
const op = "authkit.Auth.RevokeSession"
|
||||
hash, ok := parseSecret(prefixSession, plaintext)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if err := a.deps.Sessions.DeleteSession(ctx, hash); err != nil {
|
||||
return errx.Wrap(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RevokeAllUserSessions kills every active session for the user and bumps
|
||||
// the user's session_version (invalidating outstanding JWT access tokens).
|
||||
func (a *Auth) RevokeAllUserSessions(ctx context.Context, userID uuid.UUID) error {
|
||||
const op = "authkit.Auth.RevokeAllUserSessions"
|
||||
if err := a.deps.Sessions.DeleteUserSessions(ctx, userID); err != nil {
|
||||
return errx.Wrap(op, err)
|
||||
}
|
||||
if _, err := a.deps.Users.BumpSessionVersion(ctx, userID); err != nil {
|
||||
return errx.Wrap(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SessionCookie builds an *http.Cookie pre-configured from Config. Pass the
|
||||
// plaintext returned by IssueSession; pass the matching ExpiresAt from the
|
||||
// returned *Session as `expires`. To clear a cookie at logout, pass an empty
|
||||
// plaintext and a past expiry.
|
||||
func (a *Auth) SessionCookie(plaintext string, expires time.Time) *http.Cookie {
|
||||
c := &http.Cookie{
|
||||
Name: a.cfg.SessionCookieName,
|
||||
Value: plaintext,
|
||||
Path: a.cfg.SessionCookiePath,
|
||||
Domain: a.cfg.SessionCookieDomain,
|
||||
Secure: a.cfg.SessionCookieSecure,
|
||||
HttpOnly: a.cfg.SessionCookieHTTPOnly,
|
||||
SameSite: a.cfg.SessionCookieSameSite,
|
||||
Expires: expires,
|
||||
}
|
||||
if plaintext == "" {
|
||||
c.MaxAge = -1
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// resolveRolesAndPermissions fetches the user's role names and the union of
|
||||
// their permission names. Both are returned as flat string slices for cheap
|
||||
// containment checks on the Principal.
|
||||
func (a *Auth) resolveRolesAndPermissions(ctx context.Context, userID uuid.UUID) ([]string, []string, error) {
|
||||
roles, err := a.deps.Roles.GetUserRoles(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
perms, err := a.deps.Permissions.GetUserPermissions(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
rNames := make([]string, len(roles))
|
||||
for i, r := range roles {
|
||||
rNames[i] = r.Name
|
||||
}
|
||||
pNames := make([]string, len(perms))
|
||||
for i, p := range perms {
|
||||
pNames[i] = p.Name
|
||||
}
|
||||
return rNames, pNames, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue