mirror of
				https://github.com/elyby/chrly.git
				synced 2025-05-31 14:11:51 +05:30 
			
		
		
		
	
		
			
				
	
	
		
			107 lines
		
	
	
		
			2.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			107 lines
		
	
	
		
			2.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package security
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"net/http"
 | |
| 	"slices"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/golang-jwt/jwt/v5"
 | |
| 
 | |
| 	"ely.by/chrly/internal/version"
 | |
| )
 | |
| 
 | |
| var now = time.Now
 | |
| var signingMethod = jwt.SigningMethodHS256
 | |
| 
 | |
| type Scope string
 | |
| 
 | |
| const (
 | |
| 	ProfilesScope Scope = "profiles"
 | |
| 	SignScope     Scope = "sign"
 | |
| )
 | |
| 
 | |
| var validScopes = []Scope{
 | |
| 	ProfilesScope,
 | |
| 	SignScope,
 | |
| }
 | |
| 
 | |
| type claims struct {
 | |
| 	jwt.RegisteredClaims
 | |
| 	Scopes []Scope `json:"scopes"`
 | |
| }
 | |
| 
 | |
| func NewJwt(key []byte) *Jwt {
 | |
| 	return &Jwt{
 | |
| 		Key: key,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type Jwt struct {
 | |
| 	Key []byte
 | |
| }
 | |
| 
 | |
| func (t *Jwt) NewToken(scopes ...Scope) (string, error) {
 | |
| 	if len(scopes) == 0 {
 | |
| 		return "", errors.New("you must specify at least one scope")
 | |
| 	}
 | |
| 
 | |
| 	for _, scope := range scopes {
 | |
| 		if !slices.Contains(validScopes, scope) {
 | |
| 			return "", fmt.Errorf("unknown scope %s", scope)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	token := jwt.New(signingMethod)
 | |
| 	token.Claims = &claims{
 | |
| 		jwt.RegisteredClaims{
 | |
| 			Issuer:   "chrly",
 | |
| 			IssuedAt: jwt.NewNumericDate(now()),
 | |
| 		},
 | |
| 		scopes,
 | |
| 	}
 | |
| 	token.Header["v"] = version.MajorVersion
 | |
| 
 | |
| 	return token.SignedString(t.Key)
 | |
| }
 | |
| 
 | |
| // Keep those names generic in order to reuse them in future for alternative authentication methods
 | |
| var MissingAuthenticationError = errors.New("authentication value not provided")
 | |
| var InvalidTokenError = errors.New("passed authentication value is invalid")
 | |
| 
 | |
| func (t *Jwt) Authenticate(req *http.Request, scope Scope) error {
 | |
| 	bearerToken := req.Header.Get("Authorization")
 | |
| 	if bearerToken == "" {
 | |
| 		return MissingAuthenticationError
 | |
| 	}
 | |
| 
 | |
| 	if !strings.HasPrefix(strings.ToLower(bearerToken), "bearer ") {
 | |
| 		return InvalidTokenError
 | |
| 	}
 | |
| 
 | |
| 	tokenStr := bearerToken[7:] // trim "bearer " part
 | |
| 	token, err := jwt.ParseWithClaims(tokenStr, &claims{}, func(token *jwt.Token) (interface{}, error) {
 | |
| 		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
 | |
| 			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
 | |
| 		}
 | |
| 
 | |
| 		return t.Key, nil
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return errors.Join(InvalidTokenError, err)
 | |
| 	}
 | |
| 
 | |
| 	if _, vHeaderExists := token.Header["v"]; !vHeaderExists {
 | |
| 		return errors.Join(InvalidTokenError, errors.New("missing v header"))
 | |
| 	}
 | |
| 
 | |
| 	claims := token.Claims.(*claims)
 | |
| 	if !slices.Contains(claims.Scopes, scope) {
 | |
| 		return errors.New("the token doesn't have the scope to perform the action")
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 |