auth/internal/domain/token.go

139 lines
2.9 KiB
Go

package domain
import (
"fmt"
"testing"
"time"
"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/jwt"
"github.com/stretchr/testify/require"
http "github.com/valyala/fasthttp"
"source.toby3d.me/website/indieauth/internal/random"
)
type (
// Token describes the data of the token used by the clients.
Token struct {
AccessToken string
ClientID *ClientID
Me *Me
Scope Scopes
}
NewTokenOptions struct {
Algorithm string
Expiration time.Duration
Issuer *ClientID
NonceLength int
Scope Scopes
Secret interface{}
Subject *Me
}
)
var DefaultNewTokenOptions = NewTokenOptions{
NonceLength: 32,
Algorithm: "HS256",
}
func NewToken(opts NewTokenOptions) (*Token, error) {
if opts.NonceLength == 0 {
opts.NonceLength = DefaultNewTokenOptions.NonceLength
}
if opts.Algorithm == "" {
opts.Algorithm = DefaultNewTokenOptions.Algorithm
}
now := time.Now().UTC().Round(time.Second)
nonce, err := random.String(opts.NonceLength)
if err != nil {
return nil, fmt.Errorf("cannot generate nonce: %w", err)
}
t := jwt.New()
t.Set(jwt.IssuerKey, opts.Issuer.String())
t.Set(jwt.SubjectKey, opts.Subject.String())
t.Set(jwt.NotBeforeKey, now)
t.Set(jwt.IssuedAtKey, now)
t.Set("scope", opts.Scope)
t.Set("nonce", nonce)
if opts.Expiration != 0 {
t.Set(jwt.ExpirationKey, now.Add(opts.Expiration))
}
accessToken, err := jwt.Sign(t, jwa.SignatureAlgorithm(opts.Algorithm), opts.Secret)
if err != nil {
return nil, fmt.Errorf("cannot sign a new access token: %w", err)
}
return &Token{
AccessToken: string(accessToken),
ClientID: opts.Issuer,
Me: opts.Subject,
Scope: opts.Scope,
}, err
}
// TestToken returns a valid Token with the generated test data filled in.
func TestToken(tb testing.TB) *Token {
tb.Helper()
nonce, err := random.String(42)
require.NoError(tb, err)
t := jwt.New()
cid := TestClientID(tb)
me := TestMe(tb)
now := time.Now().UTC().Round(time.Second)
scope := []Scope{
ScopeCreate,
ScopeUpdate,
ScopeDelete,
}
// NOTE(toby3d): required
t.Set(jwt.IssuerKey, cid.String())
t.Set(jwt.SubjectKey, me.me.String())
// TODO(toby3d): t.Set(jwt.AudienceKey, nil)
t.Set(jwt.ExpirationKey, now.Add(1*time.Hour))
t.Set(jwt.NotBeforeKey, now.Add(-1*time.Hour))
t.Set(jwt.IssuedAtKey, now.Add(-1*time.Hour))
// TODO(toby3d): t.Set(jwt.JwtIDKey, nil)
// optional
t.Set("scope", scope)
t.Set("nonce", nonce)
accessToken, err := jwt.Sign(t, jwa.HS256, []byte("hackme"))
require.NoError(tb, err)
return &Token{
ClientID: cid,
Me: me,
Scope: scope,
AccessToken: string(accessToken),
}
}
// SetAuthHeader writes an Access Token to the request header.
func (t *Token) SetAuthHeader(r *http.Request) {
if t.AccessToken == "" {
return
}
r.Header.Set(http.HeaderAuthorization, t.String())
}
func (t *Token) String() string {
if t.AccessToken == "" {
return ""
}
return "Bearer " + string(t.AccessToken)
}