Skip to content

Commit

Permalink
feat(Auth): (70) Added auth package (#77)
Browse files Browse the repository at this point in the history
* started some expiriments with auth pkg

* demo for auth pkg

* made a small demo, updated some auth logic

* update

* update

* update

* some review comments solved

* added go files

* added tests

* removed demo

* updated tests

* update go files

* added test

* remove log

* fix review comments

* review comments

* fix go mod merge conflict

---------

Co-authored-by: Tim van Osch <[email protected]>
  • Loading branch information
JEFFTheDev and TimVosch authored Jan 15, 2024
1 parent 10ec546 commit 4644983
Show file tree
Hide file tree
Showing 10 changed files with 1,246 additions and 0 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ require (
github.com/fission/fission v1.19.0
github.com/fxamacker/cbor/v2 v2.4.0
github.com/go-chi/chi/v5 v5.0.7
github.com/go-jose/go-jose/v3 v3.0.1
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang-migrate/migrate/v4 v4.16.2
github.com/google/uuid v1.3.0
github.com/gorilla/schema v1.2.0
Expand Down
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
Expand All @@ -118,6 +120,8 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
Expand Down Expand Up @@ -151,6 +155,7 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
Expand Down Expand Up @@ -307,6 +312,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
Expand Down Expand Up @@ -347,6 +353,7 @@ go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
Expand Down
19 changes: 19 additions & 0 deletions pkg/auth/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package auth

import (
"net/http"

"sensorbucket.nl/sensorbucket/internal/web"
)

var (
// Authorization errors
ErrUnauthorized = web.NewError(http.StatusUnauthorized, "Unauthorized", "UNAUTHORIZED")
ErrNoTenantIDFound = web.NewError(http.StatusForbidden, "Forbidden", "FORBIDDEN")
ErrNoPermissions = web.NewError(http.StatusForbidden, "Forbidden", "FORBIDDEN")
ErrPermissionsNotGranted = web.NewError(http.StatusForbidden, "Forbidden", "FORBIDDEN")
ErrNoUserID = web.NewError(http.StatusForbidden, "Forbidden", "FORBIDDEN")

// Request and server errors
ErrAuthHeaderInvalidFormat = web.NewError(http.StatusBadRequest, "Authorization header must be formatted as 'Bearer {token}'", "AUTH_HEADER_INVALID_FORMAT")
)
169 changes: 169 additions & 0 deletions pkg/auth/middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package auth

import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"time"

"github.com/go-jose/go-jose/v3"
"github.com/golang-jwt/jwt"
"sensorbucket.nl/sensorbucket/internal/web"
)

type ctxKey int

type claims struct {
TenantID int64 `json:"tid"`
Permissions []permission `json:"perms"`
UserID int64 `json:"uid"`
Expiration int64 `json:"exp"`
}

func (c *claims) Valid() error {
for _, permission := range c.Permissions {
if permission.Valid() != nil {
return fmt.Errorf("invalid permissions")
}
}
if c.TenantID > 0 && c.UserID > 0 && c.Expiration > time.Now().Unix() {
return nil
}
return fmt.Errorf("claims not valid")
}

type jwksClient interface {
Get() (jose.JSONWebKeySet, error)
}

type jwksHttpClient struct {
issuer string
httpClient http.Client
}

func (c *jwksHttpClient) Get() (jose.JSONWebKeySet, error) {
res, err := c.httpClient.Get(fmt.Sprintf("%s/.well-known/jwks.json", c.issuer))

if err != nil {
return jose.JSONWebKeySet{}, fmt.Errorf("failed to fetch jwks: %w", err)
}
var jwks jose.JSONWebKeySet
if err := json.NewDecoder(res.Body).Decode(&jwks); err != nil {
return jose.JSONWebKeySet{}, fmt.Errorf("failed to decode jwks: %w", err)
}
return jwks, nil
}

type contextBuilder struct {
c context.Context
}

func (cb *contextBuilder) With(key ctxKey, value any) *contextBuilder {
cb.c = context.WithValue(cb.c, key, value)
return cb
}

func (cb *contextBuilder) Finish() context.Context {
return cb.c
}

const (
ctxUserID ctxKey = iota
ctxCurrentTenantID
ctxPermissions
)

func NewJWKSHttpClient(issuer string) *jwksHttpClient {
return &jwksHttpClient{
issuer: issuer,
httpClient: http.Client{},
}
}

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(), ctxCurrentTenantID)
_, permissionsPresent := fromRequestContext[[]permission](r.Context(), ctxPermissions)
_, userIDPresent := fromRequestContext[int64](r.Context(), ctxUserID)
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 there is no JWT or if the JWT is OK
// Anonymous requests are allowed by this handler
func Authenticate(keyClient jwksClient) 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 == "" {
// Allow anonymous requests
next.ServeHTTP(w, r)
return
}
tokenStr, ok := strings.CutPrefix(auth, "Bearer ")
if !ok {
web.HTTPError(w, ErrAuthHeaderInvalidFormat)
return
}

// Retrieve the JWT and ensure it was signed by us
c := claims{}
token, err := jwt.ParseWithClaims(tokenStr, &c, validateJWTFunc(keyClient))
if err == nil && token.Valid {
// JWT itself is validated, pass it to the actual endpoint for further authorization
// First fill the context with user information
cb := contextBuilder{c: r.Context()}
next.ServeHTTP(w, r.WithContext(
cb.
With(ctxCurrentTenantID, []int64{c.TenantID}).
With(ctxUserID, c.UserID).
With(ctxPermissions, c.Permissions).
Finish()))
return
}
log.Printf("[Error] authentication failed err: %s", err)
web.HTTPError(w, ErrUnauthorized)
})
}
}

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

// Retrieve JWKS
jwks, err := jwksClient.Get()
if err != nil {
return nil, fmt.Errorf("failed to retrieve jwks: %w", err)
}

// Look for the key as indicated by the token key id
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
}
}
Loading

0 comments on commit 4644983

Please sign in to comment.