146 lines
3.2 KiB
Go
146 lines
3.2 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 {
|
|
Scope Scopes
|
|
ClientID *ClientID
|
|
Me *Me
|
|
AccessToken string
|
|
}
|
|
|
|
// NewTokenOptions contains options for NewToken function.
|
|
NewTokenOptions struct {
|
|
Expiration time.Duration
|
|
Scope Scopes
|
|
Issuer *ClientID
|
|
Subject *Me
|
|
Secret []byte
|
|
Algorithm string
|
|
NonceLength int
|
|
}
|
|
)
|
|
|
|
//nolint: gochecknoglobals
|
|
var DefaultNewTokenOptions = NewTokenOptions{
|
|
Algorithm: "HS256",
|
|
NonceLength: 32,
|
|
}
|
|
|
|
// NewToken create a new token by provided options.
|
|
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.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.Issuer != nil {
|
|
t.Set(jwt.IssuerKey, opts.Issuer.String())
|
|
}
|
|
|
|
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 valid random generated token for tests.
|
|
func TestToken(tb testing.TB) *Token {
|
|
tb.Helper()
|
|
|
|
nonce, err := random.String(22)
|
|
require.NoError(tb, err)
|
|
|
|
t := jwt.New()
|
|
cid := TestClientID(tb)
|
|
me := TestMe(tb, "https://user.example.net/")
|
|
now := time.Now().UTC().Round(time.Second)
|
|
scope := Scopes{
|
|
ScopeCreate,
|
|
ScopeDelete,
|
|
ScopeUpdate,
|
|
}
|
|
|
|
// NOTE(toby3d): required
|
|
t.Set(jwt.IssuerKey, cid.String())
|
|
t.Set(jwt.SubjectKey, 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())
|
|
}
|
|
|
|
// String returns string representation of token.
|
|
func (t Token) String() string {
|
|
if t.AccessToken == "" {
|
|
return ""
|
|
}
|
|
|
|
return "Bearer " + string(t.AccessToken)
|
|
}
|