mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-15 16:02:45 +00:00
87 lines
3.5 KiB
Go
87 lines
3.5 KiB
Go
package token
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/warmbly/warmbly/internal/errx"
|
|
"github.com/warmbly/warmbly/internal/models"
|
|
"github.com/warmbly/warmbly/internal/pkg/crypt"
|
|
)
|
|
|
|
// AdminDeviceIssuer keeps delegation separate from ordinary sign-in minting.
|
|
type AdminDeviceIssuer interface {
|
|
GenerateAdminDeviceSession(context.Context, uuid.UUID, uuid.UUID, time.Time) (*models.Token, *errx.Error)
|
|
}
|
|
|
|
func validAdminDeviceProof(mfa bool, revoked, expires, reauth *time.Time, proofAt, now time.Time) bool {
|
|
return mfa && revoked == nil && expires != nil && expires.After(now) && reauth != nil &&
|
|
!proofAt.After(now) && now.Sub(proofAt) <= ReauthWindow && !reauth.Before(proofAt) && !reauth.After(now)
|
|
}
|
|
|
|
func (s *tokenService) GenerateAdminDeviceSession(ctx context.Context, sourceID, userID uuid.UUID, proofAt time.Time) (*models.Token, *errx.Error) {
|
|
tx, err := s.db.Begin(ctx)
|
|
if err != nil {
|
|
return nil, errx.InternalError()
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
var source models.Session
|
|
var perms uint32
|
|
var banScope uint64
|
|
// Lock both authorities through insertion so revocation and permission changes serialize with issuance.
|
|
err = tx.QueryRow(ctx, `SELECT s.auth_provider, s.mfa_verified, s.revoked_at, s.expires_at, s.reauth_at,
|
|
u.admin_permissions, u.ban_scope FROM sessions s JOIN users u ON u.id=s.user_id
|
|
WHERE s.id=$1 AND s.user_id=$2 FOR UPDATE OF s, u`, sourceID, userID).Scan(
|
|
&source.AuthProvider, &source.MFAVerified, &source.RevokedAt, &source.ExpiresAt, &source.ReauthAt, &perms, &banScope)
|
|
now := time.Now().UTC()
|
|
if err != nil || perms == 0 || banScope&uint64(models.BanScopeLogin) != 0 || !validAdminDeviceProof(source.MFAVerified, source.RevokedAt, source.ExpiresAt, source.ReauthAt, proofAt, now) {
|
|
return nil, errx.ErrUnauthorized
|
|
}
|
|
accessNonce, err := crypt.Nonce()
|
|
if err != nil {
|
|
return nil, errx.InternalError()
|
|
}
|
|
refreshNonce, err := crypt.Nonce()
|
|
if err != nil {
|
|
return nil, errx.InternalError()
|
|
}
|
|
expires := now.Add(RefreshTokenLifeTime)
|
|
session := &models.Session{ID: uuid.New(), UserID: userID, AuthProvider: source.AuthProvider,
|
|
MFAVerified: source.MFAVerified, ReauthAt: &proofAt, CreatedAt: now, LastRefreshedAt: now,
|
|
ExpiresAt: &expires, AccessNonce: accessNonce, RefreshNonce: refreshNonce, BrowserName: "warmblyctl", OSName: "CLI"}
|
|
tok := &models.Token{AccessTokenExpiresAt: now.Add(AccessTokenLifeTime), RefreshTokenExpiresAt: expires}
|
|
tok.AccessToken, err = s.GenerateTokenFor(PurposeAccess, userID, session.ID, "", accessNonce, now, tok.AccessTokenExpiresAt)
|
|
if err != nil {
|
|
return nil, errx.InternalError()
|
|
}
|
|
tok.RefreshToken, err = s.GenerateTokenFor(PurposeRefresh, userID, session.ID, "", refreshNonce, now, expires)
|
|
if err != nil {
|
|
return nil, errx.InternalError()
|
|
}
|
|
if xerr := s.tokenRepository.GenerateSession(ctx, tx, session); xerr != nil {
|
|
return nil, xerr
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return nil, errx.InternalError()
|
|
}
|
|
if s.signInAlert != nil {
|
|
alerter := s.signInAlert
|
|
go func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
|
defer cancel()
|
|
alerter.NewSignIn(ctx, userID, session.BrowserName, session.OSName, "", "")
|
|
}()
|
|
}
|
|
if s.sessionObserver != nil {
|
|
observer := s.sessionObserver
|
|
start := SessionStart{UserID: userID, Browser: session.BrowserName, OS: session.OSName, AuthProvider: source.AuthProvider, MFAVerified: source.MFAVerified, NewDevice: true}
|
|
go func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
|
defer cancel()
|
|
observer.SessionStarted(ctx, start)
|
|
}()
|
|
}
|
|
return tok, nil
|
|
}
|