Skip to content

Build docker image workflow #7

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

Open
wants to merge 3 commits into
base: master
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
44 changes: 44 additions & 0 deletions .github/workflows/build-docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Build Docker image
on: [release, pull_request]
jobs:
build:
name: Build Docker
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Docker meta
id: meta
uses: crazy-max/ghaction-docker-meta@v2
with:
# list of Docker images to use as base name for tags
images: |
${{ github.repository_owner }}/nginx-jwt-auth
ghcr.io/${{ github.repository_owner }}/nginx-jwt-auth
# generate Docker tags based on the following events/attributes
tags: |
type=schedule
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
-
name: Login to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Build and push
uses: docker/build-push-action@v2
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
4 changes: 2 additions & 2 deletions .github/workflows/build-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ jobs:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.12
- name: Set up Go 1.16
uses: actions/setup-go@v1
with:
go-version: 1.12
go-version: 1.16
id: go

- name: Check out code
Expand Down
16 changes: 16 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "debug",
"args": ["--insecure", "--insecure-addr=0.0.0.0:8888"],
"program": "${workspaceFolder}"
}
]
}
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.12 AS build
FROM golang:1.16 AS build
WORKDIR /src
COPY ["go.mod", "go.sum", "./"]
RUN go mod download
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ Flag | Description | Default
## Configuration file
The service takes a configuration file in YAML format, by default `config.yaml`. For example:

TODO: update documentaion for `jwksUrl` validation keys are no longer needed and instead `jwksUrl` shoudl be used:
```yaml
jwksUrl: https://<<insert_openid_connect_host>>/.well-known/openid-configuration/jwks
#No longer working
validationKeys:
- type: ecPublicKey
key: |
Expand Down
Binary file added __debug_bin
Binary file not shown.
7 changes: 7 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
jwksUrl: https://<<insert_openid_connect_host>>/.well-known/openid-configuration/jwks
claimsSource: queryString
# claims:
# - groups:
# - developers
# - administrators

7 changes: 5 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
module github.com/carlpett/nginx-subrequest-auth-jwt

go 1.12
go 1.16

require (
github.com/MicahParks/keyfunc v0.3.3
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/juliangruber/go-intersect v1.0.0
github.com/prometheus/client_golang v1.1.0
go.uber.org/atomic v1.4.0 // indirect
go.uber.org/multierr v1.1.0 // indirect
go.uber.org/zap v1.10.0
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/yaml.v2 v2.2.1
gopkg.in/yaml.v2 v2.2.2
)
352 changes: 352 additions & 0 deletions go.sum

Large diffs are not rendered by default.

95 changes: 70 additions & 25 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package main

import (
"crypto/ecdsa"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"time"

"github.com/carlpett/nginx-subrequest-auth-jwt/logger"

"github.com/MicahParks/keyfunc"
"github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request"
"github.com/juliangruber/go-intersect"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"gopkg.in/alecthomas/kingpin.v2"
Expand Down Expand Up @@ -47,11 +49,11 @@ func init() {
)
}

type server struct {
PublicKey *ecdsa.PublicKey
Logger logger.Logger
ClaimsSource string
StaticClaims []map[string][]string
type server struct {
Jwks keyfunc.JWKS
Logger logger.Logger
ClaimsSource string
StaticClaims []map[string][]string
}

func newServer(logger logger.Logger, configFilePath string) (*server, error) {
Expand All @@ -66,22 +68,39 @@ func newServer(logger logger.Logger, configFilePath string) (*server, error) {
return nil, err
}

// TODO: Only supports a single EC PubKey for now
pubkey, err := jwt.ParseECPublicKeyFromPEM([]byte(config.ValidationKeys[0].KeyMaterial))
jwksUrl := config.JwksUrl;
// Create the keyfunc options. Refresh the JWKS every hour and log errors.
refreshInterval := time.Hour
options := keyfunc.Options{
RefreshInterval: &refreshInterval,
RefreshErrorHandler: func(err error) {
log.Printf("There was an error with the jwt.KeyFunc\nError: %s", err.Error())
},
}
// Create the JWKS from the resource at the given URL.
// jwks will be refreshed according to time interval set in options
jwks, err := keyfunc.Get(jwksUrl, options)
if err != nil {
return nil, err

return nil, fmt.Errorf("failed to create JWKS from resource at the given URL.\nError: %s", err.Error())
}


//pubkey, err := jwt.ParseECPublicKeyFromPEM([]byte(config.ValidationKeys[0].KeyMaterial))
// if err != nil {
// return nil, err
// }

if !contains([]string{"static", "queryString"}, config.ClaimsSource) {
return nil, fmt.Errorf("claimsSource parameter must be set and either 'static' or 'queryString'")
}

if config.ClaimsSource == claimsSourceStatic && len(config.StaticClaims) == 0 {
return nil, fmt.Errorf("Claims configuration is empty")
return nil, fmt.Errorf("claims configuration is empty")
}

return &server{
PublicKey: pubkey,
Jwks: *jwks,
Logger: logger,
ClaimsSource: config.ClaimsSource,
StaticClaims: config.StaticClaims,
Expand All @@ -94,6 +113,7 @@ type validationKey struct {
}

type config struct {
JwksUrl string `yaml:"jwksUrl"`
ValidationKeys []validationKey `yaml:"validationKeys"`
ClaimsSource string `yaml:"claimsSource"`
StaticClaims []map[string][]string `yaml:"claims"`
Expand Down Expand Up @@ -185,18 +205,25 @@ func (s *server) validate(rw http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}


func (s *server) validateDeviceToken(r *http.Request) bool {
t := time.Now()
defer validationTime.Observe(time.Since(t).Seconds())

var claims jwt.MapClaims
token, err := request.ParseFromRequestWithClaims(r, request.AuthorizationHeaderExtractor, &claims, func(token *jwt.Token) (interface{}, error) {
// TODO: Only supports EC for now
if _, ok := token.Method.(*jwt.SigningMethodECDSA); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return s.PublicKey, nil
})
jwtB64, err :=request.AuthorizationHeaderExtractor.ExtractToken(r);
if err !=nil{
s.Logger.Debugw("Failed to extract token from Autorization header", "err", err)
}
token ,err := jwt.Parse(jwtB64, s.Jwks.KeyFunc)

// token, err := request.ParseFromRequestWithClaims(r, request.AuthorizationHeaderExtractor, &claims, func(token *jwt.Token) (interface{}, error) {

// // TODO: Only supports EC for now
// if _, ok := token.Method.(*jwt.SigningMethodECDSA); !ok {
// return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
// }
// return s.ActivePublicKey, nil
// })
if err != nil {
s.Logger.Debugw("Failed to parse token", "err", err)
return false
Expand All @@ -205,16 +232,16 @@ func (s *server) validateDeviceToken(r *http.Request) bool {
s.Logger.Debugw("Invalid token", "token", token.Raw)
return false
}
if err := claims.Valid(); err != nil {
if err := token.Claims.Valid(); err != nil {
s.Logger.Debugw("Got invalid claims", "err", err)
return false
}

switch s.ClaimsSource {
case claimsSourceStatic:
return s.staticClaimValidator(claims)
return s.staticClaimValidator(token.Claims.(jwt.MapClaims))
case claimsSourceQueryString:
return s.queryStringClaimValidator(claims, r)
return s.queryStringClaimValidator(token.Claims.(jwt.MapClaims), r)
default:
s.Logger.Errorw("Configuration error: Unhandled claims source", "claimsSource", s.ClaimsSource)
return false
Expand Down Expand Up @@ -257,10 +284,28 @@ func (s *server) queryStringClaimValidator(claims jwt.MapClaims, r *http.Request

passedValidation := true
for claimName, validValues := range validClaims {
actual, ok := claims[strings.TrimPrefix(claimName, "claims_")].(string)
if !ok || !contains(validValues, actual) {
passedValidation = false
claimObj := claims[strings.TrimPrefix(claimName, "claims_")]

switch claimVal := claimObj.(type) {
case string:
if !contains(validValues, claimVal) {
passedValidation = false
}
case []interface{}:
actualClaims := make([]string, len(claimVal))
for i, e := range claimVal {
claim := e.(string)
actualClaims[i] = claim;
}
intersectResult :=intersect.Simple(validValues,actualClaims);
// all required scopes from the query string must match
if len(intersectResult.([]interface{})) != len(validValues) {
passedValidation = false
}
default:
fmt.Errorf("I don't know how to handle claim object %T\n", claimObj)
}

}

if !passedValidation {
Expand Down