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: verify jwt on request #5

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
HELM_ARCHITECT_CHARTS_PATH=
HELM_ARCHITECT_CHARTS_PATH=
HELM_ARCHITECT_JWT_CERTIFICATE=[x509 certificate of your OAuth2 Provider]
1 change: 1 addition & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
!.env.example

backend
jwt_certificate.pem
1 change: 1 addition & 0 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ require (
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.1.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
Expand Down
2 changes: 2 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
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/v5 v5.1.0 h1:UGKbA/IPjtS6zLcdB7i5TyACMgSbOTiR8qzXgw8HWQU=
github.com/golang-jwt/jwt/v5 v5.1.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
Expand Down
17 changes: 6 additions & 11 deletions backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@ func main() {

server := gin.Default()

// Cross Origin Resource Sharing
corsCfg := cors.DefaultConfig()
corsCfg.AllowAllOrigins = true
corsCfg.AllowHeaders = append(corsCfg.AllowHeaders, "x-requested-with")
corsCfg.AllowHeaders = append(corsCfg.AllowHeaders, "x-requested-with") // x-requested-with is required by the client of the frontend
server.Use(cors.New(corsCfg))

auth.RegisterUserRoutes(server)
// Authenticate and Authorize requests
server.Use(auth.Authenticated(auth.DefaultAuthConfig()))

charts.RegisterChartRoutes(server)
releases.RegisterChartRoutes(server)

Expand All @@ -57,15 +60,7 @@ func checkEnv() error {
return err
}

if _, err := utils.EnvOrError(auth.OIDC_URL); err != nil {
return err
}

if _, err := utils.EnvOrError(auth.OIDC_CLIENT_ID); err != nil {
return err
}

if _, err := utils.EnvOrError(auth.OIDC_CLIENT_SECRET); err != nil {
if _, err := utils.EnvOrError(auth.JWT_CERTIFICATE); err != nil {
return err
}

Expand Down
168 changes: 74 additions & 94 deletions backend/pkg/auth/auth.go
Original file line number Diff line number Diff line change
@@ -1,130 +1,110 @@
package auth

import (
"context"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net/http"
"os"
"strings"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin"
"golang.org/x/oauth2"
"github.com/golang-jwt/jwt/v5"
"github.com/natrontech/helmarchitect/backend/internal/utils"
)

const OIDC_URL = "HELM_ARCHITECT_OIDC_URL"
const OIDC_CLIENT_ID = "HELM_ARCHITECT_OIDC_CLIENT_ID"
const OIDC_CLIENT_SECRET = "HELM_ARCHITECT_OIDC_CLIENT_SECRET"
const JWT_CERTIFICATE = "HELM_ARCHITECT_JWT_CERTIFICATE"

type AuthProvider interface {
// Authorize is to be called by the callback in the authorization flow
Authorize(*http.Response) (*oauth2.Token, error)
Token() error
UserInfo() error
Verify(*oauth2.Token) bool
}

type OIDProvider struct {
AuthProvider
const AUTHORIZATION_HEADER = "Authorization"
const AUTHORIZATION_SCHEME = "Bearer"

authProviderCfg *AuthProviderConfig
oauthCfg *oauth2.Config
provider *oidc.Provider
}
var jwtParser = jwt.NewParser()

type AuthProviderConfig struct {
Ctx context.Context
ProviderUrl string
ClientId string
ClientSecret string
RedirectUrl string
type Config interface {
CertificatePath() string
Certificate(string) *x509.Certificate
}

func NewAuthProvider(cfg *AuthProviderConfig) (AuthProvider, error) {

oidcProvider, err := oidc.NewProvider(cfg.Ctx, cfg.ProviderUrl)
if err != nil {
return nil, err
}

provider := new(OIDProvider)
provider.provider = oidcProvider
provider.authProviderCfg = cfg
type defaultConfig struct {
Config

return provider, nil
certificate *x509.Certificate
}

func newAuthConfig(clientId string, clientSecret string, redirectUrl string, provider *oidc.Provider) *oauth2.Config {

cfg := new(oauth2.Config)
cfg.ClientID = clientId
cfg.ClientSecret = clientSecret
cfg.RedirectURL = redirectUrl
cfg.Endpoint = provider.Endpoint()
cfg.Scopes = []string{oidc.ScopeOpenID, "email", "profile", "groups"}
func (cfg *defaultConfig) CertificatePath() string {

return cfg
return os.Getenv(JWT_CERTIFICATE)
}

func RegisterUserRoutes(e *gin.Engine) {
func (cfg *defaultConfig) Certificate(certificatePath string) *x509.Certificate {

if certificatePath != "" {
if c, err := utils.FileOpen(certificatePath, os.O_RDONLY); err == nil {
if stat, err := c.Stat(); err == nil {
fileSize := stat.Size()
bytes := make([]byte, fileSize)
if n, err := c.Read(bytes); err == nil && n == int(fileSize) {
if pemBlock, _ := pem.Decode(bytes); pemBlock != nil {
if certificate, err := x509.ParseCertificate(pemBlock.Bytes); err == nil {
cfg.certificate = certificate
return certificate
} else {
fmt.Println(err.Error())
}
} else {
fmt.Println(err.Error())
}
} else {
fmt.Println(err.Error())
}
} else {
fmt.Println(err.Error())
}
} else {
fmt.Println(err.Error())
}
}

e.GET("/api/alpha/login", login)
panic("unable to read jwt certificate")
}

// createRevision creates a new revision of the chart
//
// @Summary create a new revision of the chart
// @Tags charts
// @Accept json
// @Produce json
// @Param name path string true "name of the chart"
// @Param Revision body Revision true "revision object to be created"
// @Success 200 {object} Revision
// @Failure 400 {object} utils.ApiError
// @Failure 404 {object} utils.ApiError
// @Failure 500 {object} utils.ApiError
// @Router /api/alpha/chart/{name}/revision [post]
func login(c *gin.Context) {
func DefaultAuthConfig() Config {

return new(defaultConfig)
}

func (provider *OIDProvider) Authorize(response *http.Response) (*oauth2.Token, error) {
func Authenticated(cfg Config) gin.HandlerFunc {

t, err := provider.oauthCfg.Exchange(provider.authProviderCfg.Ctx, response.Request.URL.Query().Get("code"))
if err != nil {
return nil, err
}
return func(c *gin.Context) {

if !provider.Verify(t) {
return nil, errors.New("failed verifying token")
}
if authorization := c.Request.Header.Get(AUTHORIZATION_HEADER); authorization != "" {

return t, err
}
if !strings.HasPrefix(authorization, AUTHORIZATION_SCHEME) {
c.AbortWithError(http.StatusUnauthorized, utils.NewApiError(errors.New("expected bearer scheme"), http.StatusUnauthorized, c, "TRACE-ID"))
return
}

func (provider *OIDProvider) Token() error {
// [authentikurl]/application/o/token/
return nil
}
cert := cfg.Certificate(cfg.CertificatePath())
accessToken := strings.Trim(strings.Split(authorization, AUTHORIZATION_SCHEME)[1], " ")
token, err := jwtParser.Parse(accessToken, func(t *jwt.Token) (interface{}, error) { return cert.PublicKey, nil })
if err != nil {
c.AbortWithError(http.StatusUnauthorized, utils.NewApiError(err, http.StatusUnauthorized, c, "TRACE-ID"))
return
}

func (provider *OIDProvider) UserInfo() error {
// [authentikurl]/application/o/userinfo/
return nil
}
if !authorized(token) {
c.AbortWithError(http.StatusForbidden, utils.NewApiError(err, http.StatusForbidden, c, "TRACE-ID"))
return
}

func (provider *OIDProvider) Verify(token *oauth2.Token) bool {

rawToken, ok := token.Extra("id_token").(string)
if !ok {
return false
}

oidcConfig := &oidc.Config{
ClientID: provider.oauthCfg.ClientID,
}

_, err := provider.provider.Verifier(oidcConfig).Verify(provider.authProviderCfg.Ctx, rawToken)
if err != nil {
return false
} else {
c.AbortWithError(http.StatusUnauthorized, utils.NewApiError(errors.New("http authorization expected"), http.StatusUnauthorized, c, "TRACE-ID"))
return
}
}
}

func authorized(t *jwt.Token) bool {
return true
}