Files
warmbly/internal/app/token/admin_device.go
T

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
}