mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-12-20 20:01:17 +00:00
Allow to issue generic tokens with variable expiry
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
0cb761ae10
commit
f743d56d14
9 changed files with 114 additions and 49 deletions
|
@ -3,7 +3,6 @@ package main
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/Luzifer/go_helpers/v2/str"
|
||||
"github.com/Luzifer/twitch-bot/v3/internal/service/authcache"
|
||||
)
|
||||
|
||||
|
@ -23,17 +22,11 @@ func authBackendInternalAppToken(token string) (modules []string, expiresAt time
|
|||
}
|
||||
|
||||
func authBackendInternalEditorToken(token string) ([]string, time.Time, error) {
|
||||
id, user, expiresAt, err := editorTokenService.ValidateLoginToken(token)
|
||||
_, _, expiresAt, modules, err := editorTokenService.ValidateLoginToken(token)
|
||||
if err != nil {
|
||||
// None of our tokens: Nay.
|
||||
return nil, time.Time{}, authcache.ErrUnauthorized
|
||||
}
|
||||
|
||||
if !str.StringInSlice(user, config.BotEditors) && !str.StringInSlice(id, config.BotEditors) {
|
||||
// That user is none of our editors: Deny access
|
||||
return nil, time.Time{}, authcache.ErrUnauthorized
|
||||
}
|
||||
|
||||
// Editors have full access: Return module "*"
|
||||
return []string{"*"}, expiresAt, nil
|
||||
return modules, expiresAt, nil
|
||||
}
|
||||
|
|
16
botEditor.go
16
botEditor.go
|
@ -1,21 +1,23 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/Luzifer/twitch-bot/v3/pkg/twitch"
|
||||
)
|
||||
|
||||
func getAuthorizationFromRequest(r *http.Request) (string, *twitch.Client, error) {
|
||||
func getAuthorizationFromRequest(r *http.Request) (string, error) {
|
||||
token := r.Header.Get("Authorization")
|
||||
if token == "" {
|
||||
return "", nil, errors.New("no authorization provided")
|
||||
return "", fmt.Errorf("no authorization provided")
|
||||
}
|
||||
|
||||
tc := twitch.New(cfg.TwitchClient, cfg.TwitchClientSecret, token, "")
|
||||
_, user, _, _, err := editorTokenService.ValidateLoginToken(token) //nolint:dogsled // Required at other places
|
||||
|
||||
_, user, err := tc.GetAuthorizedUser(r.Context())
|
||||
return user, tc, errors.Wrap(err, "getting authorized user")
|
||||
if user == "" {
|
||||
user = "API-User"
|
||||
}
|
||||
|
||||
return user, errors.Wrap(err, "getting authorized user")
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ func registerEditorAutoMessageRoutes() {
|
|||
}
|
||||
|
||||
func configEditorHandleAutoMessageAdd(w http.ResponseWriter, r *http.Request) {
|
||||
user, _, err := getAuthorizationFromRequest(r)
|
||||
user, err := getAuthorizationFromRequest(r)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -104,7 +104,7 @@ func configEditorHandleAutoMessageAdd(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func configEditorHandleAutoMessageDelete(w http.ResponseWriter, r *http.Request) {
|
||||
user, _, err := getAuthorizationFromRequest(r)
|
||||
user, err := getAuthorizationFromRequest(r)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -141,7 +141,7 @@ func configEditorHandleAutoMessagesGet(w http.ResponseWriter, _ *http.Request) {
|
|||
}
|
||||
|
||||
func configEditorHandleAutoMessageUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
user, _, err := getAuthorizationFromRequest(r)
|
||||
user, err := getAuthorizationFromRequest(r)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
|
|
@ -103,7 +103,7 @@ func registerEditorGeneralConfigRoutes() {
|
|||
}
|
||||
|
||||
func configEditorHandleGeneralAddAuthToken(w http.ResponseWriter, r *http.Request) {
|
||||
user, _, err := getAuthorizationFromRequest(r)
|
||||
user, err := getAuthorizationFromRequest(r)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -169,7 +169,7 @@ func configEditorHandleGeneralAuthURLs(w http.ResponseWriter, _ *http.Request) {
|
|||
}
|
||||
|
||||
func configEditorHandleGeneralDeleteAuthToken(w http.ResponseWriter, r *http.Request) {
|
||||
user, _, err := getAuthorizationFromRequest(r)
|
||||
user, err := getAuthorizationFromRequest(r)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -232,7 +232,7 @@ func configEditorHandleGeneralListAuthTokens(w http.ResponseWriter, _ *http.Requ
|
|||
}
|
||||
|
||||
func configEditorHandleGeneralUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
user, _, err := getAuthorizationFromRequest(r)
|
||||
user, err := getAuthorizationFromRequest(r)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
|
|
@ -207,7 +207,8 @@ func configEditorGlobalLogin(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
tok, expiresAt, err := editorTokenService.CreateLoginToken(id, user)
|
||||
// Bot-Editors do have unlimited access to all modules: Pass in module `*`
|
||||
tok, expiresAt, err := editorTokenService.CreateUserToken(id, user, []string{"*"})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -228,12 +229,12 @@ func configEditorGlobalRefreshToken(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, "invalid renew request", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
id, user, _, err := editorTokenService.ValidateLoginToken(token)
|
||||
id, user, _, modules, err := editorTokenService.ValidateLoginToken(token)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
tok, expiresAt, err := editorTokenService.CreateLoginToken(id, user)
|
||||
tok, expiresAt, err := editorTokenService.CreateUserToken(id, user, modules)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ func registerEditorRulesRoutes() {
|
|||
}
|
||||
|
||||
func configEditorRulesAdd(w http.ResponseWriter, r *http.Request) {
|
||||
user, _, err := getAuthorizationFromRequest(r)
|
||||
user, err := getAuthorizationFromRequest(r)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -117,7 +117,7 @@ func configEditorRulesAdd(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func configEditorRulesDelete(w http.ResponseWriter, r *http.Request) {
|
||||
user, _, err := getAuthorizationFromRequest(r)
|
||||
user, err := getAuthorizationFromRequest(r)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -154,7 +154,7 @@ func configEditorRulesGet(w http.ResponseWriter, _ *http.Request) {
|
|||
}
|
||||
|
||||
func configEditorRulesUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
user, _, err := getAuthorizationFromRequest(r)
|
||||
user, err := getAuthorizationFromRequest(r)
|
||||
if err != nil {
|
||||
http.Error(w, errors.Wrap(err, "getting authorized user").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
|
|
@ -93,6 +93,12 @@ backendLoop:
|
|||
ce.ExpiresAt = time.Now().Add(negativeCacheTime)
|
||||
}
|
||||
|
||||
if ce.ExpiresAt.IsZero() {
|
||||
// Infinite valid token, we should periodically re-check and
|
||||
// therefore cache for the negativeCacheTime
|
||||
ce.ExpiresAt = time.Now().Add(negativeCacheTime)
|
||||
}
|
||||
|
||||
s.lock.Lock()
|
||||
s.cache[s.cacheKey(token)] = &ce
|
||||
s.lock.Unlock()
|
||||
|
|
|
@ -20,14 +20,15 @@ const (
|
|||
|
||||
type (
|
||||
claims struct {
|
||||
TwitchUser twitchUser `json:"twitchUser"`
|
||||
Modules []string `json:"modules"`
|
||||
TwitchUser *twitchUser `json:"twitchUser,omitempty"`
|
||||
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
twitchUser struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
// Service manages the permission database
|
||||
|
@ -39,11 +40,12 @@ func New(db database.Connector) *Service {
|
|||
return &Service{db}
|
||||
}
|
||||
|
||||
// CreateLoginToken packs user-id and user name into a JWT, signs it
|
||||
// CreateUserToken packs user-id and user name into a JWT, signs it
|
||||
// and returns the signed token
|
||||
func (s Service) CreateLoginToken(id, user string) (token string, expiresAt time.Time, err error) {
|
||||
func (s Service) CreateUserToken(id, user string, modules []string) (token string, expiresAt time.Time, err error) {
|
||||
cl := claims{
|
||||
TwitchUser: twitchUser{
|
||||
Modules: modules,
|
||||
TwitchUser: &twitchUser{
|
||||
ID: id,
|
||||
Name: user,
|
||||
},
|
||||
|
@ -57,23 +59,40 @@ func (s Service) CreateLoginToken(id, user string) (token string, expiresAt time
|
|||
},
|
||||
}
|
||||
|
||||
tok := jwt.NewWithClaims(&jwt.SigningMethodEd25519{}, cl)
|
||||
|
||||
priv, err := s.getSigningKey()
|
||||
if err != nil {
|
||||
return "", expiresAt, fmt.Errorf("getting signing key: %w", err)
|
||||
}
|
||||
|
||||
if token, err = tok.SignedString(priv); err != nil {
|
||||
return "", expiresAt, fmt.Errorf("signing token: %w", err)
|
||||
if token, err = s.createTokenFromClaims(cl); err != nil {
|
||||
return "", time.Time{}, fmt.Errorf("creating token: %w", err)
|
||||
}
|
||||
|
||||
return token, cl.ExpiresAt.Time, nil
|
||||
}
|
||||
|
||||
// CreateGenericModuleToken creates a non-user-bound token with the
|
||||
// given modules. Pass in 0 validity to create a non-expiring token.
|
||||
func (s Service) CreateGenericModuleToken(modules []string, validity time.Duration) (token string, err error) {
|
||||
cl := claims{
|
||||
Modules: modules,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "Twitch-Bot",
|
||||
Audience: []string{},
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
if validity > 0 {
|
||||
cl.ExpiresAt = jwt.NewNumericDate(time.Now().Add(validity))
|
||||
}
|
||||
|
||||
if token, err = s.createTokenFromClaims(cl); err != nil {
|
||||
return "", fmt.Errorf("creating token: %w", err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// ValidateLoginToken takes a token, validates it with the stored
|
||||
// key and returns the twitch-id and the user-name from the token
|
||||
func (s Service) ValidateLoginToken(token string) (id, user string, expiresAt time.Time, err error) {
|
||||
func (s Service) ValidateLoginToken(token string) (id, user string, expiresAt time.Time, modules []string, err error) {
|
||||
var cl claims
|
||||
|
||||
tok, err := jwt.ParseWithClaims(token, &cl, func(*jwt.Token) (any, error) {
|
||||
|
@ -86,16 +105,37 @@ func (s Service) ValidateLoginToken(token string) (id, user string, expiresAt ti
|
|||
})
|
||||
if err != nil {
|
||||
// Something went wrong when parsing & validating
|
||||
return "", "", expiresAt, fmt.Errorf("validating token: %w", err)
|
||||
return "", "", expiresAt, nil, fmt.Errorf("validating token: %w", err)
|
||||
}
|
||||
|
||||
if claims, ok := tok.Claims.(*claims); ok {
|
||||
if claims.ExpiresAt != nil {
|
||||
expiresAt = claims.ExpiresAt.Time
|
||||
}
|
||||
|
||||
if claims.TwitchUser == nil {
|
||||
return "", "", expiresAt, claims.Modules, nil
|
||||
}
|
||||
// We had no error and the claims are our claims
|
||||
return claims.TwitchUser.ID, claims.TwitchUser.Name, claims.ExpiresAt.Time, nil
|
||||
return claims.TwitchUser.ID, claims.TwitchUser.Name, expiresAt, claims.Modules, nil
|
||||
}
|
||||
|
||||
// We had no error but were not able to convert the claims
|
||||
return "", "", expiresAt, fmt.Errorf("unknown claims type")
|
||||
return "", "", expiresAt, nil, fmt.Errorf("unknown claims type")
|
||||
}
|
||||
|
||||
func (s Service) createTokenFromClaims(cl claims) (token string, err error) {
|
||||
tok := jwt.NewWithClaims(&jwt.SigningMethodEd25519{}, cl)
|
||||
|
||||
priv, err := s.getSigningKey()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting signing key: %w", err)
|
||||
}
|
||||
|
||||
if token, err = tok.SignedString(priv); err != nil {
|
||||
return "", fmt.Errorf("signing token: %w", err)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (s Service) getSigningKey() (priv ed25519.PrivateKey, err error) {
|
||||
|
|
|
@ -43,13 +43,36 @@ func TestTokenFlow(t *testing.T) {
|
|||
user = "example"
|
||||
)
|
||||
|
||||
tok, expiresAt, err := s.CreateLoginToken(id, user)
|
||||
tok, expiresAt, err := s.CreateUserToken(id, user, []string{"*"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, expiresAt.After(time.Now().Add(tokenValidity-time.Minute)))
|
||||
|
||||
tid, tuser, texpiresAt, err := s.ValidateLoginToken(tok)
|
||||
tid, tuser, texpiresAt, modules, err := s.ValidateLoginToken(tok)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, id, tid)
|
||||
assert.Equal(t, user, tuser)
|
||||
assert.Equal(t, expiresAt, texpiresAt)
|
||||
assert.Equal(t, []string{"*"}, modules)
|
||||
|
||||
// Generic without expiry
|
||||
tok, err = s.CreateGenericModuleToken([]string{"test"}, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
tid, tuser, texpiresAt, modules, err = s.ValidateLoginToken(tok)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", tid)
|
||||
assert.Equal(t, "", tuser)
|
||||
assert.Equal(t, time.Time{}, texpiresAt)
|
||||
assert.Equal(t, []string{"test"}, modules)
|
||||
|
||||
// Generic with expiry
|
||||
tok, err = s.CreateGenericModuleToken([]string{"test"}, time.Minute)
|
||||
require.NoError(t, err)
|
||||
|
||||
tid, tuser, texpiresAt, modules, err = s.ValidateLoginToken(tok)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", tid)
|
||||
assert.Equal(t, "", tuser)
|
||||
assert.True(t, time.Now().Add(time.Minute+time.Second).After(texpiresAt))
|
||||
assert.Equal(t, []string{"test"}, modules)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue