2022-05-02 08:19:37 -07:00
|
|
|
package auth
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/base64"
|
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
"time"
|
|
|
|
|
2022-05-14 07:52:08 -07:00
|
|
|
"codeberg.org/u1f320/pronouns.cc/backend/log"
|
2022-05-02 08:19:37 -07:00
|
|
|
"emperror.dev/errors"
|
|
|
|
"github.com/golang-jwt/jwt/v4"
|
|
|
|
"github.com/rs/xid"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Claims are the claims used in a token.
|
|
|
|
type Claims struct {
|
2022-12-31 15:34:38 -08:00
|
|
|
UserID xid.ID `json:"sub"`
|
|
|
|
TokenID xid.ID `json:"jti"`
|
|
|
|
UserIsAdmin bool `json:"adm"`
|
|
|
|
|
2023-03-08 01:32:18 -08:00
|
|
|
// APIToken specifies whether this token was generated for the API or for the website.
|
|
|
|
// API tokens cannot perform some destructive actions, such as DELETE /users/@me.
|
|
|
|
APIToken bool `json:"atn"`
|
2022-12-31 15:34:38 -08:00
|
|
|
// TokenWrite specifies whether this token can be used for write actions.
|
|
|
|
// If set to false, this token can only be used for read actions.
|
|
|
|
TokenWrite bool `json:"twr"`
|
2022-05-02 08:19:37 -07:00
|
|
|
|
|
|
|
jwt.RegisteredClaims
|
|
|
|
}
|
|
|
|
|
|
|
|
type Verifier struct {
|
|
|
|
key []byte
|
|
|
|
}
|
|
|
|
|
|
|
|
func New() *Verifier {
|
|
|
|
raw := os.Getenv("HMAC_KEY")
|
|
|
|
if raw == "" {
|
|
|
|
log.Fatal("$HMAC_KEY is not set")
|
|
|
|
}
|
|
|
|
|
|
|
|
key, err := base64.URLEncoding.DecodeString(raw)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal("$HMAC_KEY is not a valid base 64 string")
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Verifier{key: key}
|
|
|
|
}
|
|
|
|
|
2022-12-31 15:34:38 -08:00
|
|
|
// ExpireDays is after how many days the token will expire.
|
|
|
|
const ExpireDays = 30
|
2022-05-02 08:19:37 -07:00
|
|
|
|
|
|
|
// CreateToken creates a token for the given user ID.
|
|
|
|
// It expires after 30 days.
|
2023-03-08 01:32:18 -08:00
|
|
|
func (v *Verifier) CreateToken(userID, tokenID xid.ID, isAdmin bool, isAPIToken bool, isWriteToken bool) (token string, err error) {
|
2022-05-02 08:19:37 -07:00
|
|
|
now := time.Now()
|
2022-12-31 15:34:38 -08:00
|
|
|
expires := now.Add(ExpireDays * 24 * time.Hour)
|
2022-05-02 08:19:37 -07:00
|
|
|
|
2022-12-31 15:34:38 -08:00
|
|
|
t := jwt.NewWithClaims(jwt.SigningMethodHS256, Claims{
|
|
|
|
UserID: userID,
|
|
|
|
TokenID: tokenID,
|
|
|
|
UserIsAdmin: isAdmin,
|
2023-03-08 01:32:18 -08:00
|
|
|
APIToken: isAPIToken,
|
2022-12-31 15:34:38 -08:00
|
|
|
TokenWrite: isWriteToken,
|
2022-05-02 08:19:37 -07:00
|
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
|
|
Issuer: "pronouns",
|
|
|
|
ExpiresAt: jwt.NewNumericDate(expires),
|
|
|
|
IssuedAt: jwt.NewNumericDate(now),
|
|
|
|
NotBefore: jwt.NewNumericDate(now),
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
2022-12-31 15:34:38 -08:00
|
|
|
return t.SignedString(v.key)
|
2022-05-02 08:19:37 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Claims parses the given token and returns its Claims.
|
|
|
|
// If the token is invalid, returns an error.
|
|
|
|
func (v *Verifier) Claims(token string) (c Claims, err error) {
|
|
|
|
parsed, err := jwt.ParseWithClaims(token, &Claims{}, func(t *jwt.Token) (interface{}, error) {
|
|
|
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
|
|
return nil, fmt.Errorf(`unexpected signing method "%v"`, t.Header["alg"])
|
|
|
|
}
|
|
|
|
|
|
|
|
return v.key, nil
|
|
|
|
})
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return c, errors.Wrap(err, "parsing token")
|
|
|
|
}
|
|
|
|
|
|
|
|
if c, ok := parsed.Claims.(*Claims); ok && parsed.Valid {
|
|
|
|
return *c, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return c, fmt.Errorf("unknown claims type %T", parsed.Claims)
|
|
|
|
}
|