Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Auth): (70) Added auth package #77

Merged
merged 18 commits into from
Jan 15, 2024
Prev Previous commit
Next Next commit
some review comments solved
  • Loading branch information
JEFFTheDev committed Jan 8, 2024
commit 9e9050f4262ce88d4f74e8b0a87a44a9ccbd3d65
24 changes: 14 additions & 10 deletions pkg/auth/demo/demo.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func main() {
r := chi.NewRouter()

// Use middleware to validate JWT
r.Use(auth.Protect(secretKey))
r.Use(auth.Authenticate(secretKey))

t, err := createToken()
if err != nil {
Expand Down Expand Up @@ -58,36 +58,40 @@ func someProtectedEndpoint() http.HandlerFunc {
filter := Filter{
Tenants: []int64{11},
}
grant, err := auth.MustHavePermissions(r.Context(),
err := auth.MustHavePermissions(r.Context(),
auth.READ_DEVICES,
auth.WRITE_DEVICES)
if err != nil {
// unauthorized!
fmt.Println("must have permissions", err)
web.HTTPError(w, err)
return
}

if len(filter.Tenants) == 0 {
// In case the filter is left empty, the desired output is data of all tenants this user has access to
filter.Tenants = grant.GetTenants()
filter.Tenants, err = auth.GetTenants(r.Context())
fmt.Println(err)
}

if !grant.HasPermissionsFor(filter.Tenants...) {
if !auth.HasPermissionsFor(r.Context(), filter.Tenants...) {
// unauthorized!
fmt.Println("no permis")
web.HTTPError(w, auth.ErrUnauthorized)
return
}

fmt.Println("Role API_KEY_MANAGER:", grant.HasRole(auth.API_KEY_MANAGER)) // should be false
fmt.Println("Role DEVICE_MANAGER:", grant.HasRole(auth.DEVICE_MANAGER)) // should be true
apiKeyManagerRole := auth.NewRole(
auth.READ_API_KEYS,
auth.WRITE_API_KEYS)

fmt.Println("Role API_KEY_MANAGER:", auth.HasRole(r.Context(), apiKeyManagerRole)) // should be false

// Authorized!
// Both authorized to read and write and access
// to all tenants the user has permissions for or just the ones that were inputted by the user
fmt.Println("Filter", filter)
fmt.Println("Authorized!", grant)
fmt.Println("> User ID", grant.GetUser())
fmt.Println("> Tenants", grant.GetTenants())
fmt.Println("Authorized!")
// fmt.Println("> Permissions", grant.permissions)
}
}
Expand All @@ -99,10 +103,10 @@ func createToken() (string, error) {
"permissions": []string{
auth.READ_DEVICES.String(),
auth.READ_API_KEYS.String(),
auth.WRITE_API_KEYS.String(),
auth.WRITE_DEVICES.String(),
"asdsad",
},
"roles": []string{}, // ??
// tenant:123,
// device:541
"user_id": 431,
Expand Down
1 change: 0 additions & 1 deletion pkg/auth/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,5 @@ var (

// Request and server errors
ErrNoPermissionsToCheck = web.NewError(http.StatusInternalServerError, "No permissions to check", "PERMISSIONS_NOT_CONFIGURED")
ErrAuthHeaderMissing = web.NewError(http.StatusBadRequest, "Authorization header must be set", "AUTH_HEADER_MISSING")
ErrAuthHeaderInvalidFormat = web.NewError(http.StatusBadRequest, "Authorization header must be formatted as 'Bearer {token}'", "AUTH_HEADER_INVALID_FORMAT")
)
83 changes: 0 additions & 83 deletions pkg/auth/grant.go

This file was deleted.

145 changes: 81 additions & 64 deletions pkg/auth/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package auth

import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
Expand All @@ -16,53 +18,68 @@ var (
PermissionsKey = "permissions"
)

// Authentication middleware for checking the validity of a JWT
// TODO: one endpoint should optionally fill the context
// Otherone ensures context is filled with the information

// // TODO:
// // - alg header
// // - checks
// // - sb002-poc
// return secret, nil
// })

func Protect() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, tenantIdPresent := fromRequestContext[[]int64](r.Context(), CurrentTenantIdKey)
_, permissionsPresent := fromRequestContext[[]permission](r.Context(), PermissionsKey)
_, userIdPresent := fromRequestContext[int64](r.Context(), UserIdKey)
if tenantIdPresent && permissionsPresent && userIdPresent {
// All required authentication values are present, allow the request
next.ServeHTTP(w, r)
return
}
web.HTTPError(w, ErrUnauthorized)
})
}
}

// Authentication middleware for checking the validity of any present JWT
// Checks if the JWT is signed using the given secret
// Serves the next HTTP handler if all is OK
func Protect(secret []byte) func(http.Handler) http.Handler {
// Serves the next HTTP handler if there is no JWT or if the JWT is OK
// Anonymous requests are allowed by this handler
func Authenticate(secret []byte) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if auth == "" {
web.HTTPError(w, ErrAuthHeaderMissing)
// No authorization header present, return OK since there is no info to extract
// anonymous requests are allowed
return
}
if !strings.Contains(auth, "Bearer ") {
tokenStr, ok := strings.CutPrefix(auth, "Bearer ")
if !ok {
web.HTTPError(w, ErrAuthHeaderInvalidFormat)
return
}
tokenStr := strings.TrimPrefix(auth, "Bearer ")

// Retrieve the JWT and ensure it was signed by us
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
return secret, nil
})
c := claims{}
token, err := jwt.ParseWithClaims(tokenStr, &c, validateJwt)
// token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {

// TODO: test if expired is relevant
if err == nil && token.Valid && token.Claims.Valid() == nil {
if claims, ok := token.Claims.(jwt.MapClaims); ok {
expired := !claims.VerifyExpiresAt(time.Now().Unix(), true)
if !expired {
tenantId, ok := currentTenantFromClaims(claims)
if !ok {
web.HTTPError(w, ErrNoTenantIdFound)
return
}
permissions, ok := permissionsFromClaims(claims)
if !ok {
web.HTTPError(w, ErrNoPermissions)
return
}
userId, ok := userFromClaims(claims)
if !ok {
web.HTTPError(w, ErrNoUserId)
return
}

// JWT itself is validated, pass it to the actual endpoint for further authorization
// First fill the context with user information
next.ServeHTTP(w, r.WithContext(contextWithValues(r.Context(), map[string]interface{}{
CurrentTenantIdKey: tenantId,
UserIdKey: userId,
PermissionsKey: permissions,
CurrentTenantIdKey: []int64{c.TenantId},
UserIdKey: c.UserId,
PermissionsKey: c.Permissions,
})))
return
}
Expand All @@ -74,51 +91,51 @@ func Protect(secret []byte) func(http.Handler) http.Handler {
}
}

func validateJwt(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}

// Fetch jwks
res, err := http.Get("http://ok:4467/.well-known/jwks.json")
if err != nil {
return nil, fmt.Errorf("failed to fetch jwks: %w", err)
}
var jwks jose.JSONWebKeySet
if err := json.NewDecoder(res.Body).Decode(&jwks); err != nil {
return nil, fmt.Errorf("failed to decode jwks: %w", err)
}
kid, ok := token.Header["kid"].(string)
if !ok {
return nil, fmt.Errorf("no kid in token")
}
keys := jwks.Key(kid)
if len(keys) == 0 {
return nil, fmt.Errorf("no keys found for token")
}
key := keys[0]
if key.Algorithm != token.Method.Alg() {
return nil, fmt.Errorf("key alg differs from token alg: %s vs %s", key.Algorithm, token.Method.Alg())
}
return key.Public().Key, nil
}

func contextWithValues(ctx context.Context, values map[string]interface{}) context.Context {
for key, val := range values {
ctx = context.WithValue(ctx, key, val)
}
return ctx
}

func currentTenantFromClaims(claims jwt.MapClaims) (int64, bool) {
return int64FromClaims(claims, CurrentTenantIdKey)
}

func userFromClaims(claims jwt.MapClaims) (int64, bool) {
return int64FromClaims(claims, UserIdKey)
type claims struct {
TenantId int64 `json:"current_tenant_id"`
Permissions []string `json:"permissions"`
UserId int64 `json:"user_id"`
}

func int64FromClaims(claims jwt.MapClaims, key string) (int64, bool) {
val, ok := claims[key]
if ok {
// The JWT library converts the value to a float64 before it does to an int64
asFl, ok := val.(float64)
if ok {
return int64(asFl), true
}
}
return -1, false
}

func permissionsFromClaims(claims jwt.MapClaims) ([]permission, bool) {
permissions, ok := claims[PermissionsKey]
if ok {

// Permissions are given as a slice
if asSlice, ok := permissions.([]interface{}); ok {

// Each permission in the slice is of type interface but should be able to be converted to a string
res := []permission{}
for _, perm := range asSlice {
if val, ok := perm.(string); ok {
res = append(res, permission(val))
} else {
return nil, false
}
}
return res, true
}
func (c *claims) Valid() error {
if c.TenantId > 0 && c.UserId > 0 {
return nil
}
return nil, false
return fmt.Errorf("claims not valid")
}
20 changes: 5 additions & 15 deletions pkg/auth/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,12 @@ var (
// User worker permissions
READ_USER_WORKERS permission = "READ_USER_WORKERS"
WRITE_USER_WORKERS permission = "WRITE_USER_WORKERS"

// Roles
DEVICE_MANAGER role = role([]permission{
READ_DEVICES,
WRITE_DEVICES,
})
API_KEY_MANAGER role = role([]permission{
READ_API_KEYS,
WRITE_API_KEYS,
})
)

func NewRole(permissions ...permission) role {
return role(permissions)
}

type permission string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

declare new types at top of file

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

along with their methods?


func (p permission) String() string {
Expand All @@ -43,11 +37,7 @@ func (p permission) String() string {

type role []permission

func (r role) Permissions() []permission {
return []permission(r)
}

func (r role) Contains(permissions []permission) bool {
func (r role) contains(permissions []permission) bool {
for _, rolePermission := range r {
found := false
for _, p := range permissions {
Expand Down
Loading