feat: new openid connect authentication layer
Cadoles/bouncer/pipeline/pr-develop This commit looks good
Details
Cadoles/bouncer/pipeline/pr-develop This commit looks good
Details
This commit is contained in:
parent
83fcb9a39d
commit
cc02febc30
|
@ -1,4 +1,4 @@
|
||||||
FROM reg.cadoles.com/proxy_cache/library/golang:1.21.6 AS BUILD
|
FROM reg.cadoles.com/proxy_cache/library/golang:1.22.0 AS BUILD
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y make
|
&& apt-get install -y make
|
||||||
|
|
6
Makefile
6
Makefile
|
@ -19,7 +19,11 @@ OPENWRT_DEVICE ?= 192.168.1.1
|
||||||
SIEGE_URLS_FILE ?= misc/siege/urls.txt
|
SIEGE_URLS_FILE ?= misc/siege/urls.txt
|
||||||
SIEGE_CONCURRENCY ?= 100
|
SIEGE_CONCURRENCY ?= 100
|
||||||
|
|
||||||
watch: tools/modd/bin/modd deps ## Watching updated files - live reload
|
data/bootstrap.d/dummy.yml:
|
||||||
|
mkdir -p data/bootstrap.d
|
||||||
|
cp misc/bootstrap.d/dummy.yml data/bootstrap.d/dummy.yml
|
||||||
|
|
||||||
|
watch: tools/modd/bin/modd deps data/bootstrap.d/dummy.yml ## Watching updated files - live reload
|
||||||
( set -o allexport && source .env && set +o allexport && tools/modd/bin/modd )
|
( set -o allexport && source .env && set +o allexport && tools/modd/bin/modd )
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Incoming request</h1>
|
||||||
|
<h2>Headers</h2>
|
||||||
|
<table style="width: 100%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Key</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{ range $key, $val := .Request.Header }}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<b>{{ $key }}</b>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code>{{ $val }}</code>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{
|
||||||
|
end
|
||||||
|
}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h2>Cookies</h2>
|
||||||
|
<table style="width: 100%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Domain</th>
|
||||||
|
<th>Path</th>
|
||||||
|
<th>Secure</th>
|
||||||
|
<th>MaxAge</th>
|
||||||
|
<th>HttpOnly</th>
|
||||||
|
<th>Expires</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{ range $cookie := .Request.Cookies }}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<b>{{ $cookie.Name }}</b>
|
||||||
|
</td>
|
||||||
|
<td>{{ $cookie.Domain }}</td>
|
||||||
|
<td>{{ $cookie.Path }}</td>
|
||||||
|
<td>{{ $cookie.Secure }}</td>
|
||||||
|
<td>{{ $cookie.MaxAge }}</td>
|
||||||
|
<td>{{ $cookie.HttpOnly }}</td>
|
||||||
|
<td>{{ $cookie.Expires }}</td>
|
||||||
|
<td>
|
||||||
|
<code>{{ $cookie.Value }}</code>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{
|
||||||
|
end
|
||||||
|
}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,43 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
_ "embed"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed index.gohtml
|
||||||
|
indexTmpl string
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
tmpl, err := template.New("").Parse(indexTmpl)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[FATAL] %+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
data := struct {
|
||||||
|
Request *http.Request
|
||||||
|
}{
|
||||||
|
Request: r,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tmpl.Execute(w, data); err != nil {
|
||||||
|
log.Printf("[ERROR] %+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
addr := ":8082"
|
||||||
|
|
||||||
|
log.Printf("[INFO] listening on '%s'", addr)
|
||||||
|
|
||||||
|
if err := http.ListenAndServe(addr, handler); err != nil {
|
||||||
|
log.Fatalf("[FATAL] %+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,7 @@
|
||||||
### Utilisation
|
### Utilisation
|
||||||
|
|
||||||
- [(FR) - Ajouter un layer de type "file d'attente"](./fr/tutorials/add-queue-layer.md)
|
- [(FR) - Ajouter un layer de type "file d'attente"](./fr/tutorials/add-queue-layer.md)
|
||||||
|
- [(FR) - Ajouter une authentification OpenID Connect](./fr/tutorials/add-queue-layer.md)
|
||||||
- [(FR) - Amorçage d'un serveur Bouncer via la configuration](./fr/tutorials/bootstrapping.md)
|
- [(FR) - Amorçage d'un serveur Bouncer via la configuration](./fr/tutorials/bootstrapping.md)
|
||||||
- [(FR) - Intégration avec Kubernetes](./fr/tutorials/kubernetes-integration.md)
|
- [(FR) - Intégration avec Kubernetes](./fr/tutorials/kubernetes-integration.md)
|
||||||
|
|
||||||
|
|
|
@ -2,5 +2,6 @@
|
||||||
|
|
||||||
Vous trouverez ci-dessous la liste des entités "Layer" activables sur vos entité "Proxy":
|
Vous trouverez ci-dessous la liste des entités "Layer" activables sur vos entité "Proxy":
|
||||||
|
|
||||||
|
- [Authn (`authn-*`)](./authn/README.md) - Authentification des accès (SSO)
|
||||||
- [Queue](./queue.md) - File d'attente dynamique
|
- [Queue](./queue.md) - File d'attente dynamique
|
||||||
- [Circuit Breaker](./circuitbreaker.md) - Coupure d'accès à un site ou une sous section de celui ci
|
- [Circuit Breaker](./circuitbreaker.md) - Coupure d'accès à un site ou une sous section de celui ci
|
|
@ -0,0 +1,61 @@
|
||||||
|
# Les layers `authn-*`
|
||||||
|
|
||||||
|
Les layers `authn-*` permettent d'activer différents modes d'authentification au sein d'un proxy Bouncer.
|
||||||
|
|
||||||
|
Les informations liées à l'utilisateur authentifié sont ensuite injectables dans les entêtes HTTP de la requête permettant ainsi une authentification unique("SSO") basée sur les entêtes HTTP ("Trusted headers SSO").
|
||||||
|
|
||||||
|
## Layers
|
||||||
|
|
||||||
|
- [`authn-oidc`](./oidc.md) - Authentification OpenID Connect
|
||||||
|
|
||||||
|
## Schéma des options
|
||||||
|
|
||||||
|
En plus de leurs options spécifiques tous les layers `authn-*` partagent un certain nombre d'options communes.
|
||||||
|
|
||||||
|
Voir le [schéma](../../../../../internal/proxy/director/layer/authn/layer-options.json).
|
||||||
|
|
||||||
|
## Règles d'injection d'entêtes
|
||||||
|
|
||||||
|
L'option `headers.rules` permet de définir une liste de règles utilisant un DSL permettant de définir de manière dynamique quels entêtes seront injectés dans la requête transitant par le layer à destination du service distant.
|
||||||
|
|
||||||
|
La liste des instructions est exécutée séquentiellement.
|
||||||
|
|
||||||
|
Bouncer utilise le projet [`expr`](https://expr-lang.org/) comme DSL. En plus des fonctionnalités natives du langage, Bouncer ajoute un certain nombre de fonction spécifique à son contexte.
|
||||||
|
|
||||||
|
Le comportement des règles par défaut est le suivant:
|
||||||
|
|
||||||
|
1. L'ensemble des entêtes HTTP correspondant au patron `Remote-*` sont supprimés ;
|
||||||
|
2. L'identifiant de l'utilisateur identifié (`user.subject`) est exporté sous la forme de l'entête HTTP `Remote-User` ;
|
||||||
|
3. L'ensemble des attributs de l'utilisateur identifié (`user.attrs`) sont exportés sous la forme `Remote-User-Attr-<name>` où `<name>` est le nom de l'attribut en minuscule, avec les `_` transformés en `-`.
|
||||||
|
|
||||||
|
### Fonctions
|
||||||
|
|
||||||
|
#### `set_header(name string, value string)`
|
||||||
|
|
||||||
|
Définir la valeur d'une entête HTTP via son nom `name` et sa valeur `value`.
|
||||||
|
|
||||||
|
#### `del_headers(pattern string)`
|
||||||
|
|
||||||
|
Supprimer un ou plusieurs entêtes HTTP dont le nom correspond au patron `pattern`.
|
||||||
|
|
||||||
|
Le patron est défini par une chaîne comprenant un ou plusieurs caractères `*`, signifiant un ou plusieurs caractères arbitraires.
|
||||||
|
|
||||||
|
### Environnement
|
||||||
|
|
||||||
|
Les règles ont accès aux variables suivantes pendant leur exécution.
|
||||||
|
|
||||||
|
#### `user`
|
||||||
|
|
||||||
|
L'utilisateur identifié par le layer.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
// Identifiant de l'utilisateur, tel que récupéré par le layer
|
||||||
|
"subject": "<string>",
|
||||||
|
// Table associative des attributs associés à l'utilisateur
|
||||||
|
// La liste de ces attributs dépend du layer d'authentification
|
||||||
|
"attrs": {
|
||||||
|
"key": "<value>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Layer `authn-oidc`
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Ce layer permet d'ajouter une authentification OpenID Connect au service distant.
|
||||||
|
|
||||||
|
Voir le tutoriel ["Ajouter une authentification OpenID Connect"](../../../tutorials/add-oidc-authn-layer.md) pour plus d'informations quant à son utilisation.
|
||||||
|
|
||||||
|
## Type
|
||||||
|
|
||||||
|
`authn-oidc`
|
||||||
|
|
||||||
|
## Schéma des options
|
||||||
|
|
||||||
|
Les options disponibles pour le layer sont décrites via un [schéma JSON](https://json-schema.org/specification). Elles sont documentées dans le [schéma visible ici](../../../../../internal/proxy/director/layer/authn/oidc/layer-options.json).
|
||||||
|
|
||||||
|
En plus de ces options spécifiques le layer peut également être configuré via [les options communes aux layers `authn-*`](../../../../../internal/proxy/director/layer/authn/layer-options.json).
|
||||||
|
|
||||||
|
## Métriques
|
||||||
|
|
||||||
|
_Aucune [métrique Prometheus](../metrics.md) n'est exportée par ce layer._
|
|
@ -0,0 +1,62 @@
|
||||||
|
# Ajouter une authentification OpenID Connect
|
||||||
|
|
||||||
|
Dans ce tutoriel nous verrons comment ajouter un layer de type `oidc-authn` à un proxy pour ajouter une authentification OpenID Connect à notre service distant.
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
Pour réaliser ce tutoriel nous allons utiliser la forge Cadoles comme fournisseur d'identité. Vous devrez donc créer une application OAuth2 avec votre compte Cadoles sur https://forge.cadoles.com/user/settings/applications et collecter les informations suivantes:
|
||||||
|
|
||||||
|
- Identifiant du client ;
|
||||||
|
- Secret du client.
|
||||||
|
|
||||||
|
Concernant l'URL de redirection, si vous ne modifiez pas l'option `oidc.loginCallbackPath` vous devrez renseigner une URL répondant au modèle suivant:
|
||||||
|
|
||||||
|
```
|
||||||
|
<base_url>/.bouncer/authn/oidc/<proxy_name>/<layer_name>/callback
|
||||||
|
```
|
||||||
|
|
||||||
|
Où
|
||||||
|
|
||||||
|
- `<base_url>` est l'URL de base d'accès à votre instance Bouncer, par exemple `http://localhost:8080` si vous avez travaillez avec une instance Bouncer locale avec la configuration par défaut ;
|
||||||
|
- `<proxy_name>` est le nom du proxy créé dans Bouncer, dans ce tutoriel `my-proxy` ;
|
||||||
|
- `<layer_name>` est le nom du layer créé dans Bouncer, dans ce tutoriel `my-layer`.
|
||||||
|
|
||||||
|
## Étapes
|
||||||
|
|
||||||
|
1. Avec le client d'administration de Bouncer en ligne de commande, créer un nouveau proxy
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bouncer admin proxy create --proxy-name my-proxy --proxy-to https://www.cadoles.com
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Activer le proxy `my-proxy`
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bouncer admin proxy update --proxy-name my-proxy --proxy-enabled
|
||||||
|
```
|
||||||
|
|
||||||
|
3. À ce stade, vous devriez pouvoir afficher le site Cadoles en ouvrant l'URL de votre instance Bouncer, par exemple `http://localhost:8080` si vous avez travaillez avec une instance Bouncer locale avec la configuration par défaut
|
||||||
|
|
||||||
|
4. Créer un layer de type `authn-oidc` pour notre nouveau proxy
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bouncer admin layer create --proxy-name my-proxy --layer-name my-layer --layer-type authn-oidc
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Configurer le nouveau layer `my-layer` avec les options collectée dans les prérequis et l'activer
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bouncer admin layer update --proxy-name my-proxy --layer-name my-layer --layer-options '{ "oidc":{"clientId": "<clientId>", "clientSecret":"<clientSecret>", "issuerURL": "https://forge.cadoles.com/", "postLogoutRedirectURL": "<baseURL>" }}' --layer-enabled
|
||||||
|
```
|
||||||
|
|
||||||
|
Où:
|
||||||
|
|
||||||
|
- `<clientId>` est l'identifiant du client OIDC récupéré dans les prérequis ;
|
||||||
|
- `<clientSecret>` est le secret du client OIDC récupéré dans les prérequis ;
|
||||||
|
- `<postLogoutRedirectURL>` est l'URL de redirection après déconnexion, par exemple `http://localhost:8080` si vous avez travaillez avec une instance Bouncer locale avec la configuration par défaut.
|
||||||
|
|
||||||
|
6. À ce stade en ouvrant l'URL de votre instance Bouncer vous devriez être redirigé vers la forge Cadoles vous demandant de vous authentifier.
|
||||||
|
|
||||||
|
## Ressources
|
||||||
|
|
||||||
|
- [Référence du layer `authn-oidc`](../../fr/references/layers/authn/oidc.md)
|
16
go.mod
16
go.mod
|
@ -9,9 +9,13 @@ require (
|
||||||
github.com/Masterminds/sprig/v3 v3.2.3
|
github.com/Masterminds/sprig/v3 v3.2.3
|
||||||
github.com/bsm/redislock v0.9.4
|
github.com/bsm/redislock v0.9.4
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.3
|
github.com/btcsuite/btcd/btcutil v1.1.3
|
||||||
|
github.com/coreos/go-oidc/v3 v3.10.0
|
||||||
|
github.com/dchest/uniuri v1.2.0
|
||||||
github.com/drone/envsubst v1.0.3
|
github.com/drone/envsubst v1.0.3
|
||||||
|
github.com/expr-lang/expr v1.16.7
|
||||||
github.com/getsentry/sentry-go v0.22.0
|
github.com/getsentry/sentry-go v0.22.0
|
||||||
github.com/go-chi/chi/v5 v5.0.8
|
github.com/go-chi/chi/v5 v5.0.8
|
||||||
|
github.com/gorilla/sessions v1.2.2
|
||||||
github.com/jedib0t/go-pretty/v6 v6.4.6
|
github.com/jedib0t/go-pretty/v6 v6.4.6
|
||||||
github.com/mitchellh/mapstructure v1.4.1
|
github.com/mitchellh/mapstructure v1.4.1
|
||||||
github.com/oklog/ulid/v2 v2.1.0
|
github.com/oklog/ulid/v2 v2.1.0
|
||||||
|
@ -19,6 +23,7 @@ require (
|
||||||
github.com/prometheus/client_golang v1.16.0
|
github.com/prometheus/client_golang v1.16.0
|
||||||
github.com/qri-io/jsonschema v0.2.1
|
github.com/qri-io/jsonschema v0.2.1
|
||||||
github.com/redis/go-redis/v9 v9.0.4
|
github.com/redis/go-redis/v9 v9.0.4
|
||||||
|
golang.org/x/oauth2 v0.13.0
|
||||||
k8s.io/api v0.29.3
|
k8s.io/api v0.29.3
|
||||||
k8s.io/apimachinery v0.29.3
|
k8s.io/apimachinery v0.29.3
|
||||||
k8s.io/client-go v0.29.3
|
k8s.io/client-go v0.29.3
|
||||||
|
@ -42,6 +47,7 @@ require (
|
||||||
github.com/docker/go-connections v0.4.0 // indirect
|
github.com/docker/go-connections v0.4.0 // indirect
|
||||||
github.com/docker/go-units v0.4.0 // indirect
|
github.com/docker/go-units v0.4.0 // indirect
|
||||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||||
|
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
|
||||||
github.com/go-logr/logr v1.3.0 // indirect
|
github.com/go-logr/logr v1.3.0 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||||
|
@ -51,6 +57,7 @@ require (
|
||||||
github.com/google/gnostic-models v0.6.8 // indirect
|
github.com/google/gnostic-models v0.6.8 // indirect
|
||||||
github.com/google/gofuzz v1.2.0 // indirect
|
github.com/google/gofuzz v1.2.0 // indirect
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
github.com/huandu/xstrings v1.3.3 // indirect
|
github.com/huandu/xstrings v1.3.3 // indirect
|
||||||
github.com/imdario/mergo v0.3.12 // indirect
|
github.com/imdario/mergo v0.3.12 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
@ -80,10 +87,9 @@ require (
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||||
golang.org/x/net v0.19.0 // indirect
|
golang.org/x/net v0.19.0 // indirect
|
||||||
golang.org/x/oauth2 v0.10.0 // indirect
|
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
golang.org/x/time v0.3.0 // indirect
|
golang.org/x/time v0.3.0 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106 // indirect
|
google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106 // indirect
|
||||||
google.golang.org/protobuf v1.33.0 // indirect
|
google.golang.org/protobuf v1.33.0 // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
|
@ -126,10 +132,10 @@ require (
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
gitlab.com/wpetit/goweb v0.0.0-20230419082146-a94d9ed7202b
|
gitlab.com/wpetit/goweb v0.0.0-20230419082146-a94d9ed7202b
|
||||||
go.opencensus.io v0.24.0 // indirect
|
go.opencensus.io v0.24.0 // indirect
|
||||||
golang.org/x/crypto v0.17.0 // indirect
|
golang.org/x/crypto v0.19.0 // indirect
|
||||||
golang.org/x/mod v0.14.0 // indirect
|
golang.org/x/mod v0.14.0 // indirect
|
||||||
golang.org/x/sys v0.15.0 // indirect
|
golang.org/x/sys v0.17.0 // indirect
|
||||||
golang.org/x/term v0.15.0 // indirect
|
golang.org/x/term v0.17.0 // indirect
|
||||||
golang.org/x/tools v0.16.1 // indirect
|
golang.org/x/tools v0.16.1 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||||
gopkg.in/go-playground/validator.v9 v9.29.1 // indirect
|
gopkg.in/go-playground/validator.v9 v9.29.1 // indirect
|
||||||
|
|
32
go.sum
32
go.sum
|
@ -132,6 +132,8 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH
|
||||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||||
github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg=
|
github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg=
|
||||||
github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM=
|
github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM=
|
||||||
|
github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
|
||||||
|
github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
|
||||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||||
|
@ -146,6 +148,8 @@ github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g=
|
||||||
|
github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY=
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
||||||
|
@ -179,6 +183,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
|
||||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/expr-lang/expr v1.16.7 h1:gCIiHt5ODA0xIaDbD0DPKyZpM9Drph3b3lolYAYq2Kw=
|
||||||
|
github.com/expr-lang/expr v1.16.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||||
|
@ -199,6 +205,8 @@ github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3Bop
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
||||||
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
||||||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||||
|
@ -317,7 +325,11 @@ github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAk
|
||||||
github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
|
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||||
|
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
|
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
|
||||||
|
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
@ -547,8 +559,8 @@ golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPh
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
|
@ -647,8 +659,8 @@ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ
|
||||||
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
|
golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
|
||||||
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
|
golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
@ -734,13 +746,13 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
|
||||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
@ -750,6 +762,7 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
@ -857,8 +870,9 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
|
||||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
|
||||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||||
|
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
|
|
@ -28,17 +28,27 @@ func (s *Server) bootstrapProxies(ctx context.Context) error {
|
||||||
logger.Info(ctx, "bootstrapping proxies")
|
logger.Info(ctx, "bootstrapping proxies")
|
||||||
|
|
||||||
for proxyName, proxyConfig := range s.bootstrapConfig.Proxies {
|
for proxyName, proxyConfig := range s.bootstrapConfig.Proxies {
|
||||||
|
loopCtx := logger.With(ctx, logger.F("proxyName", proxyName), logger.F("proxyFrom", proxyConfig.From), logger.F("proxyTo", proxyConfig.To))
|
||||||
|
|
||||||
_, err := s.proxyRepository.GetProxy(ctx, proxyName)
|
_, err := s.proxyRepository.GetProxy(ctx, proxyName)
|
||||||
if !errors.Is(err, store.ErrNotFound) {
|
if !errors.Is(err, store.ErrNotFound) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info(ctx, "ignoring existing proxy", logger.F("proxyName", proxyName))
|
if proxyConfig.Recreate {
|
||||||
|
logger.Info(loopCtx, "force recreating proxy")
|
||||||
|
|
||||||
|
if err := s.deleteProxyAndLayers(ctx, proxyName); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Info(loopCtx, "ignoring existing proxy")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.Info(ctx, "creating proxy", logger.F("proxyName", proxyName))
|
logger.Info(loopCtx, "creating proxy")
|
||||||
|
|
||||||
if _, err := proxyRepo.CreateProxy(ctx, proxyName, string(proxyConfig.To), proxyConfig.From...); err != nil {
|
if _, err := proxyRepo.CreateProxy(ctx, proxyName, string(proxyConfig.To), proxyConfig.From...); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
|
|
|
@ -101,7 +101,7 @@ func (s *Server) deleteProxy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
if err := s.proxyRepository.DeleteProxy(ctx, proxyName); err != nil {
|
if err := s.deleteProxyAndLayers(ctx, proxyName); err != nil {
|
||||||
if errors.Is(err, store.ErrNotFound) {
|
if errors.Is(err, store.ErrNotFound) {
|
||||||
api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil)
|
api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil)
|
||||||
|
|
||||||
|
@ -114,23 +114,6 @@ func (s *Server) deleteProxy(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
layers, err := s.layerRepository.QueryLayers(ctx, proxyName)
|
|
||||||
if err != nil {
|
|
||||||
logAndCaptureError(ctx, "could not query proxy's layers", errors.WithStack(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, layer := range layers {
|
|
||||||
if err := s.layerRepository.DeleteLayer(ctx, proxyName, layer.Name); err != nil {
|
|
||||||
logAndCaptureError(ctx, "could not delete layer", errors.WithStack(err))
|
|
||||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.DataResponse(w, http.StatusOK, DeleteProxyResponse{
|
api.DataResponse(w, http.StatusOK, DeleteProxyResponse{
|
||||||
ProxyName: proxyName,
|
ProxyName: proxyName,
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) deleteProxyAndLayers(ctx context.Context, proxyName store.ProxyName) error {
|
||||||
|
if err := s.proxyRepository.DeleteProxy(ctx, proxyName); err != nil {
|
||||||
|
if !errors.Is(err, store.ErrNotFound) {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layers, err := s.layerRepository.QueryLayers(ctx, proxyName)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, layer := range layers {
|
||||||
|
if err := s.layerRepository.DeleteLayer(ctx, proxyName, layer.Name); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -52,6 +52,7 @@ type BootstrapProxyConfig struct {
|
||||||
To InterpolatedString `yaml:"to"`
|
To InterpolatedString `yaml:"to"`
|
||||||
From InterpolatedStringSlice `yaml:"from"`
|
From InterpolatedStringSlice `yaml:"from"`
|
||||||
Layers map[store.LayerName]BootstrapLayerConfig `yaml:"layers"`
|
Layers map[store.LayerName]BootstrapLayerConfig `yaml:"layers"`
|
||||||
|
Recreate InterpolatedBool `yaml:"recreate"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BootstrapLayerConfig struct {
|
type BootstrapLayerConfig struct {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package director
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -12,6 +13,7 @@ type contextKey string
|
||||||
const (
|
const (
|
||||||
contextKeyProxy contextKey = "proxy"
|
contextKeyProxy contextKey = "proxy"
|
||||||
contextKeyLayers contextKey = "layers"
|
contextKeyLayers contextKey = "layers"
|
||||||
|
contextKeyOriginalURL contextKey = "originalURL"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -19,6 +21,19 @@ var (
|
||||||
errUnexpectedContextValue = errors.New("unexpected context value")
|
errUnexpectedContextValue = errors.New("unexpected context value")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func withOriginalURL(ctx context.Context, url *url.URL) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKeyOriginalURL, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func OriginalURL(ctx context.Context) (*url.URL, error) {
|
||||||
|
url, err := ctxValue[*url.URL](ctx, contextKeyOriginalURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return url, nil
|
||||||
|
}
|
||||||
|
|
||||||
func withProxy(ctx context.Context, proxy *store.Proxy) context.Context {
|
func withProxy(ctx context.Context, proxy *store.Proxy) context.Context {
|
||||||
return context.WithValue(ctx, contextKeyProxy, proxy)
|
return context.WithValue(ctx, contextKeyProxy, proxy)
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,9 @@ func (d *Director) rewriteRequest(r *http.Request) (*http.Request, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
url := getRequestURL(r)
|
url := getRequestURL(r)
|
||||||
ctx = logger.With(r.Context(), logger.F("url", url.String()))
|
|
||||||
|
ctx = withOriginalURL(ctx, url)
|
||||||
|
ctx = logger.With(ctx, logger.F("url", url.String()))
|
||||||
|
|
||||||
var match *store.Proxy
|
var match *store.Proxy
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package authn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnauthorized = errors.New("unauthorized")
|
||||||
|
ErrForbidden = errors.New("forbidden")
|
||||||
|
ErrSkipRequest = errors.New("skip request")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Authenticator interface {
|
||||||
|
Authenticate(w http.ResponseWriter, r *http.Request, layer *store.Layer) (*User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PreAuthentication interface {
|
||||||
|
PreAuthentication(w http.ResponseWriter, r *http.Request, layer *store.Layer) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostAuthentication interface {
|
||||||
|
PostAuthentication(w http.ResponseWriter, r *http.Request, layer *store.Layer, user *User) error
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
package authn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
|
||||||
|
"github.com/expr-lang/expr"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (l *Layer) getRuleOptions(r *http.Request) []expr.Option {
|
||||||
|
options := make([]expr.Option, 0)
|
||||||
|
|
||||||
|
setHeader := expr.Function(
|
||||||
|
"set_header",
|
||||||
|
func(params ...any) (any, error) {
|
||||||
|
name := params[0].(string)
|
||||||
|
rawValue := params[1]
|
||||||
|
|
||||||
|
var value string
|
||||||
|
switch v := rawValue.(type) {
|
||||||
|
case []string:
|
||||||
|
value = strings.Join(v, ",")
|
||||||
|
case time.Time:
|
||||||
|
value = strconv.FormatInt(v.UTC().Unix(), 10)
|
||||||
|
case time.Duration:
|
||||||
|
value = strconv.FormatInt(int64(v.Seconds()), 10)
|
||||||
|
default:
|
||||||
|
value = fmt.Sprintf("%v", rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header.Set(name, value)
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
new(func(string, string) bool),
|
||||||
|
)
|
||||||
|
|
||||||
|
options = append(options, setHeader)
|
||||||
|
|
||||||
|
delHeaders := expr.Function(
|
||||||
|
"del_headers",
|
||||||
|
func(params ...any) (any, error) {
|
||||||
|
pattern := params[0].(string)
|
||||||
|
deleted := false
|
||||||
|
|
||||||
|
for key := range r.Header {
|
||||||
|
if !wildcard.Match(key, pattern) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header.Del(key)
|
||||||
|
deleted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleted, nil
|
||||||
|
},
|
||||||
|
new(func(string) bool),
|
||||||
|
)
|
||||||
|
|
||||||
|
options = append(options, delHeaders)
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Layer) injectHeaders(r *http.Request, options *LayerOptions, user *User) error {
|
||||||
|
rules := options.Headers.Rules
|
||||||
|
if len(rules) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
env := map[string]any{
|
||||||
|
"user": user,
|
||||||
|
}
|
||||||
|
|
||||||
|
rulesOptions := l.getRuleOptions(r)
|
||||||
|
|
||||||
|
for i, r := range rules {
|
||||||
|
program, err := expr.Compile(r, rulesOptions...)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "could not compile header rule #%d", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := expr.Run(program, env); err != nil {
|
||||||
|
return errors.Wrapf(err, "could not execute header rule #%d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"$id": "https://forge.cadoles.com/cadoles/bouncer/schemas/authn-options",
|
||||||
|
"title": "Options de configuration commune des layers 'authn-*'",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"matchURLs": {
|
||||||
|
"title": "Liste de filtrage des URLs sur lesquelles le layer est actif.",
|
||||||
|
"description": "Par exemple, si vous souhaitez limiter votre layer à l'ensemble d'une section '`/blog`' d'un site, vous pouvez déclarer la valeur `['*/blog*']`. Les autres URLs du site ne seront pas affectées par ce layer.",
|
||||||
|
"default": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"headers": {
|
||||||
|
"title": "Options de configuration du mécanisme d'injection d'entêtes HTTP liés à l'authentification",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"rules": {
|
||||||
|
"title": "Liste des règles définissant les actions d'injection/réécriture d'entêtes HTTP",
|
||||||
|
"description": "Voir la documentation (ficher 'doc/fr/references/layers/authn/README.md', section 'Règles d'injection d'entêtes') pour plus d'informations sur le fonctionnement des règles",
|
||||||
|
"type": "array",
|
||||||
|
"default": [
|
||||||
|
"del_headers('Remote-*')",
|
||||||
|
"set_header('Remote-User', user.subject)",
|
||||||
|
"map( toPairs(user.attrs), { let name = replace(lower(string(get(#, 0))), '_', '-'); set_header('Remote-User-Attr-' + name, get(#, 1)) })"
|
||||||
|
],
|
||||||
|
"item": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
package authn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/go-proxy"
|
||||||
|
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Layer struct {
|
||||||
|
layerType store.LayerType
|
||||||
|
auth Authenticator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
options, err := fromStoreOptions(layer.Options)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not parse layer options", logger.E(errors.WithStack(err)))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if preAuth, ok := l.auth.(PreAuthentication); ok {
|
||||||
|
if err := preAuth.PreAuthentication(w, r, layer); err != nil {
|
||||||
|
if errors.Is(err, ErrSkipRequest) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Error(ctx, "could not execute pre-auth hook", logger.E(errors.WithStack(err)))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
matches := wildcard.MatchAny(r.URL.String(), options.MatchURLs...)
|
||||||
|
if !matches {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := l.auth.Authenticate(w, r, layer)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrSkipRequest) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Error(ctx, "could not authenticate user", logger.E(errors.WithStack(err)))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := l.injectHeaders(r, options, user); err != nil {
|
||||||
|
logger.Error(ctx, "could not inject headers", logger.E(errors.WithStack(err)))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if postAuth, ok := l.auth.(PostAuthentication); ok {
|
||||||
|
if err := postAuth.PostAuthentication(w, r, layer, user); err != nil {
|
||||||
|
if errors.Is(err, ErrSkipRequest) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Error(ctx, "could not execute post-auth hook", logger.E(errors.WithStack(err)))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LayerType implements director.MiddlewareLayer
|
||||||
|
func (l *Layer) LayerType() store.LayerType {
|
||||||
|
return l.layerType
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLayer(layerType store.LayerType, auth Authenticator) *Layer {
|
||||||
|
return &Layer{
|
||||||
|
layerType: layerType,
|
||||||
|
auth: auth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ director.MiddlewareLayer = &Layer{}
|
|
@ -0,0 +1,92 @@
|
||||||
|
package authn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LayerOptions struct {
|
||||||
|
MatchURLs []string `mapstructure:"matchURLs"`
|
||||||
|
Headers HeadersOptions `mapstructure:"headers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeadersOptions struct {
|
||||||
|
Rules []string `mapstructure:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultLayerOptions() LayerOptions {
|
||||||
|
return LayerOptions{
|
||||||
|
MatchURLs: []string{"*"},
|
||||||
|
Headers: HeadersOptions{
|
||||||
|
Rules: []string{
|
||||||
|
"del_headers('Remote-*')",
|
||||||
|
"set_header('Remote-User', user.subject)",
|
||||||
|
`map(
|
||||||
|
toPairs(user.attrs), {
|
||||||
|
let name = replace(lower(string(get(#, 0))), '_', '-');
|
||||||
|
set_header(
|
||||||
|
'Remote-User-Attr-' + name,
|
||||||
|
get(#, 1)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) {
|
||||||
|
layerOptions := DefaultLayerOptions()
|
||||||
|
|
||||||
|
if err := FromStoreOptions(storeOptions, &layerOptions); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &layerOptions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromStoreOptions(storeOptions store.LayerOptions, dest any) error {
|
||||||
|
config := mapstructure.DecoderConfig{
|
||||||
|
Result: dest,
|
||||||
|
ZeroFields: true,
|
||||||
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||||
|
toDurationHookFunc(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder, err := mapstructure.NewDecoder(&config)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := decoder.Decode(storeOptions); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toDurationHookFunc() mapstructure.DecodeHookFunc {
|
||||||
|
return func(
|
||||||
|
f reflect.Type,
|
||||||
|
t reflect.Type,
|
||||||
|
data interface{}) (interface{}, error) {
|
||||||
|
if t != reflect.TypeOf(*new(time.Duration)) {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch f.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
return time.ParseDuration(data.(string))
|
||||||
|
case reflect.Int64:
|
||||||
|
return time.Duration(data.(int64) * int64(time.Second)), nil
|
||||||
|
default:
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
// Convert it by parsing
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,257 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||||
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Authenticator struct {
|
||||||
|
store sessions.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authenticator) PreAuthentication(w http.ResponseWriter, r *http.Request, layer *store.Layer) error {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
originalURL, err := director.OriginalURL(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
options, err := fromStoreOptions(layer.Options)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sess, err := a.store.Get(r, a.getCookieName(options.Cookie.Name, layer.Name))
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not retrieve session", logger.E(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectURL := a.getRedirectURL(layer.Proxy, layer.Name, originalURL, options)
|
||||||
|
logoutURL := a.getLogoutURL(layer.Proxy, layer.Name, originalURL, options)
|
||||||
|
|
||||||
|
client, err := a.getClient(options, redirectURL.String())
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.URL.Path {
|
||||||
|
case redirectURL.Path:
|
||||||
|
if err := client.HandleCallback(w, r, sess); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case logoutURL.Path:
|
||||||
|
if err := client.HandleLogout(w, r, sess, options.OIDC.PostLogoutRedirectURL); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate implements authn.Authenticator.
|
||||||
|
func (a *Authenticator) Authenticate(w http.ResponseWriter, r *http.Request, layer *store.Layer) (*authn.User, error) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
originalURL, err := director.OriginalURL(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
options, err := fromStoreOptions(layer.Options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sess, err := a.store.Get(r, a.getCookieName(options.Cookie.Name, layer.Name))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := sess.Save(r, w); err != nil {
|
||||||
|
logger.Error(ctx, "could not save session", logger.E(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
sess.Options.Domain = options.Cookie.Domain
|
||||||
|
sess.Options.HttpOnly = options.Cookie.HTTPOnly
|
||||||
|
sess.Options.MaxAge = int(options.Cookie.MaxAge.Seconds())
|
||||||
|
sess.Options.Path = options.Cookie.Path
|
||||||
|
|
||||||
|
switch options.Cookie.SameSite {
|
||||||
|
case "lax":
|
||||||
|
sess.Options.SameSite = http.SameSiteLaxMode
|
||||||
|
case "strict":
|
||||||
|
sess.Options.SameSite = http.SameSiteStrictMode
|
||||||
|
case "none":
|
||||||
|
sess.Options.SameSite = http.SameSiteNoneMode
|
||||||
|
default:
|
||||||
|
sess.Options.SameSite = http.SameSiteDefaultMode
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectURL := a.getRedirectURL(layer.Proxy, layer.Name, originalURL, options)
|
||||||
|
|
||||||
|
client, err := a.getClient(options, redirectURL.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idToken, err := client.Authenticate(w, r, sess)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrLoginRequired) {
|
||||||
|
return nil, errors.WithStack(authn.ErrSkipRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := a.toUser(idToken, layer.Proxy, layer.Name, originalURL, options, sess)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type claims struct {
|
||||||
|
Issuer string `json:"iss"`
|
||||||
|
Subject string `json:"sub"`
|
||||||
|
Expiration int64 `json:"exp"`
|
||||||
|
IssuedAt int64 `json:"iat"`
|
||||||
|
AuthTime int64 `json:"auth_time"`
|
||||||
|
Nonce string `json:"nonce"`
|
||||||
|
ACR string `json:"acr"`
|
||||||
|
AMR string `json:"amr"`
|
||||||
|
AZP string `json:"amp"`
|
||||||
|
Others map[string]any `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c claims) AsAttrs() map[string]any {
|
||||||
|
attrs := make(map[string]any)
|
||||||
|
|
||||||
|
for key, val := range c.Others {
|
||||||
|
if val != nil {
|
||||||
|
attrs["claim_"+key] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs["claim_iss"] = c.Issuer
|
||||||
|
attrs["claim_sub"] = c.Subject
|
||||||
|
attrs["claim_exp"] = c.Expiration
|
||||||
|
attrs["claim_iat"] = c.IssuedAt
|
||||||
|
|
||||||
|
if c.AuthTime != 0 {
|
||||||
|
attrs["claim_auth_time"] = c.AuthTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Nonce != "" {
|
||||||
|
attrs["claim_nonce"] = c.Nonce
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ACR != "" {
|
||||||
|
attrs["claim_arc"] = c.ACR
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.AMR != "" {
|
||||||
|
attrs["claim_amr"] = c.AMR
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.AZP != "" {
|
||||||
|
attrs["claim_azp"] = c.AZP
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authenticator) toUser(idToken *oidc.IDToken, proxyName store.ProxyName, layerName store.LayerName, originalURL *url.URL, options *LayerOptions, sess *sessions.Session) (*authn.User, error) {
|
||||||
|
var claims claims
|
||||||
|
|
||||||
|
if err := idToken.Claims(&claims); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := idToken.Claims(&claims.Others); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := claims.AsAttrs()
|
||||||
|
|
||||||
|
logoutURL := a.getLogoutURL(proxyName, layerName, originalURL, options)
|
||||||
|
attrs["logout_url"] = logoutURL.String()
|
||||||
|
|
||||||
|
if accessToken, exists := sess.Values[sessionKeyAccessToken]; exists && accessToken != nil {
|
||||||
|
attrs["access_token"] = accessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if refreshToken, exists := sess.Values[sessionKeyRefreshToken]; exists && refreshToken != nil {
|
||||||
|
attrs["refresh_token"] = refreshToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenExpiry, exists := sess.Values[sessionKeyTokenExpiry]; exists && tokenExpiry != nil {
|
||||||
|
attrs["token_expiry"] = tokenExpiry
|
||||||
|
}
|
||||||
|
|
||||||
|
user := authn.NewUser(idToken.Subject, attrs)
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authenticator) getRedirectURL(proxyName store.ProxyName, layerName store.LayerName, u *url.URL, options *LayerOptions) *url.URL {
|
||||||
|
return &url.URL{
|
||||||
|
Scheme: u.Scheme,
|
||||||
|
Host: u.Host,
|
||||||
|
Path: fmt.Sprintf(options.OIDC.LoginCallbackPath, fmt.Sprintf("%s/%s", proxyName, layerName)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authenticator) getLogoutURL(proxyName store.ProxyName, layerName store.LayerName, u *url.URL, options *LayerOptions) *url.URL {
|
||||||
|
return &url.URL{
|
||||||
|
Scheme: u.Scheme,
|
||||||
|
Host: u.Host,
|
||||||
|
Path: fmt.Sprintf(options.OIDC.LogoutPath, fmt.Sprintf("%s/%s", proxyName, layerName)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authenticator) getClient(options *LayerOptions, redirectURL string) (*Client, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if options.OIDC.SkipIssuerVerification {
|
||||||
|
ctx = oidc.InsecureIssuerURLContext(ctx, options.OIDC.IssuerURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := oidc.NewProvider(ctx, options.OIDC.IssuerURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not create oidc provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := NewClient(
|
||||||
|
WithCredentials(options.OIDC.ClientID, options.OIDC.ClientSecret),
|
||||||
|
WithProvider(provider),
|
||||||
|
WithRedirectURL(redirectURL),
|
||||||
|
WithScopes(options.OIDC.Scopes...),
|
||||||
|
WithAuthParams(options.OIDC.AuthParams),
|
||||||
|
)
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authenticator) getCookieName(cookieName string, layerName store.LayerName) string {
|
||||||
|
return fmt.Sprintf("%s_%s", cookieName, layerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ authn.PreAuthentication = &Authenticator{}
|
||||||
|
_ authn.Authenticator = &Authenticator{}
|
||||||
|
)
|
|
@ -0,0 +1,291 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||||
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
"github.com/dchest/uniuri"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sessionKeyAccessToken = "access-token"
|
||||||
|
sessionKeyRefreshToken = "refresh-token"
|
||||||
|
sessionKeyTokenExpiry = "token-expiry"
|
||||||
|
sessionKeyIDToken = "id-token"
|
||||||
|
sessionKeyPostLoginRedirectURL = "post-login-redirect-url"
|
||||||
|
sessionKeyLoginState = "login-state"
|
||||||
|
sessionKeyLoginNonce = "login-nonce"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrLoginRequired = errors.New("login required")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
oauth2 *oauth2.Config
|
||||||
|
provider *oidc.Provider
|
||||||
|
verifier *oidc.IDTokenVerifier
|
||||||
|
authParams map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Verifier() *oidc.IDTokenVerifier {
|
||||||
|
return c.verifier
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Provider() *oidc.Provider {
|
||||||
|
return c.provider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Authenticate(w http.ResponseWriter, r *http.Request, sess *sessions.Session) (*oidc.IDToken, error) {
|
||||||
|
idToken, err := c.getIDToken(r, sess)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(r.Context(), "could not retrieve idtoken", logger.E(errors.WithStack(err)))
|
||||||
|
|
||||||
|
c.login(w, r, sess)
|
||||||
|
|
||||||
|
return nil, errors.WithStack(ErrLoginRequired)
|
||||||
|
}
|
||||||
|
|
||||||
|
return idToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) login(w http.ResponseWriter, r *http.Request, sess *sessions.Session) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
state := uniuri.New()
|
||||||
|
nonce := uniuri.New()
|
||||||
|
|
||||||
|
originalURL, err := director.OriginalURL(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not retrieve original url", logger.E(errors.WithStack(err)))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sess.Values[sessionKeyLoginState] = state
|
||||||
|
sess.Values[sessionKeyLoginNonce] = nonce
|
||||||
|
sess.Values[sessionKeyPostLoginRedirectURL] = originalURL.String()
|
||||||
|
|
||||||
|
if err := sess.Save(r, w); err != nil {
|
||||||
|
logger.Error(ctx, "could not save session", logger.E(errors.WithStack(err)))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authCodeOptions := []oauth2.AuthCodeOption{}
|
||||||
|
authCodeOptions = append(authCodeOptions, oidc.Nonce(nonce))
|
||||||
|
|
||||||
|
for key, val := range c.authParams {
|
||||||
|
authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam(key, val))
|
||||||
|
}
|
||||||
|
|
||||||
|
authCodeURL := c.oauth2.AuthCodeURL(
|
||||||
|
state,
|
||||||
|
authCodeOptions...,
|
||||||
|
)
|
||||||
|
|
||||||
|
http.Redirect(w, r, authCodeURL, http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) HandleCallback(w http.ResponseWriter, r *http.Request, sess *sessions.Session) error {
|
||||||
|
token, _, rawIDToken, err := c.validate(r, sess)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "could not validate oidc token")
|
||||||
|
}
|
||||||
|
|
||||||
|
sess.Values[sessionKeyIDToken] = rawIDToken
|
||||||
|
sess.Values[sessionKeyAccessToken] = token.AccessToken
|
||||||
|
sess.Values[sessionKeyRefreshToken] = token.RefreshToken
|
||||||
|
sess.Values[sessionKeyTokenExpiry] = token.Expiry.UTC().Unix()
|
||||||
|
|
||||||
|
if err := sess.Save(r, w); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawPostLoginRedirectURL, exists := sess.Values[sessionKeyPostLoginRedirectURL]
|
||||||
|
if !exists {
|
||||||
|
return errors.Wrap(err, "could not find post login redirect url")
|
||||||
|
}
|
||||||
|
|
||||||
|
postLoginRedirectURL, ok := rawPostLoginRedirectURL.(string)
|
||||||
|
if !ok {
|
||||||
|
return errors.Wrapf(err, "unexpected value '%v' for post login redirect url", rawPostLoginRedirectURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, postLoginRedirectURL, http.StatusTemporaryRedirect)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) HandleLogout(w http.ResponseWriter, r *http.Request, sess *sessions.Session, postLogoutRedirectURL string) error {
|
||||||
|
state := uniuri.New()
|
||||||
|
sess.Values[sessionKeyLoginState] = state
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
rawIDToken, err := c.getRawIDToken(sess)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not retrieve raw id token", logger.E(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
sess.Values[sessionKeyIDToken] = nil
|
||||||
|
sess.Values[sessionKeyAccessToken] = nil
|
||||||
|
sess.Values[sessionKeyRefreshToken] = nil
|
||||||
|
sess.Values[sessionKeyTokenExpiry] = nil
|
||||||
|
sess.Options.MaxAge = -1
|
||||||
|
|
||||||
|
if err := sess.Save(r, w); err != nil {
|
||||||
|
return errors.Wrap(err, "could not save session")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rawIDToken == "" {
|
||||||
|
http.Redirect(w, r, postLogoutRedirectURL, http.StatusFound)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionEndURL, err := c.sessionEndURL(rawIDToken, state, postLogoutRedirectURL)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "could not retrieve session end url")
|
||||||
|
}
|
||||||
|
|
||||||
|
if sessionEndURL != "" {
|
||||||
|
http.Redirect(w, r, sessionEndURL, http.StatusFound)
|
||||||
|
} else {
|
||||||
|
http.Redirect(w, r, postLogoutRedirectURL, http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) sessionEndURL(idTokenHint, state, postLogoutRedirectURL string) (string, error) {
|
||||||
|
sessionEndEndpoint := &struct {
|
||||||
|
URL string `json:"end_session_endpoint"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
if err := c.provider.Claims(&sessionEndEndpoint); err != nil {
|
||||||
|
return "", errors.Wrap(err, "could not unmarshal claims")
|
||||||
|
}
|
||||||
|
|
||||||
|
if sessionEndEndpoint.URL == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString(sessionEndEndpoint.URL)
|
||||||
|
|
||||||
|
v := url.Values{}
|
||||||
|
|
||||||
|
if idTokenHint != "" {
|
||||||
|
v.Set("id_token_hint", idTokenHint)
|
||||||
|
}
|
||||||
|
|
||||||
|
if postLogoutRedirectURL != "" {
|
||||||
|
v.Set("post_logout_redirect_uri", postLogoutRedirectURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if state != "" {
|
||||||
|
v.Set("state", state)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(sessionEndEndpoint.URL, "?") {
|
||||||
|
buf.WriteByte('&')
|
||||||
|
} else {
|
||||||
|
buf.WriteByte('?')
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteString(v.Encode())
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) validate(r *http.Request, sess *sessions.Session) (*oauth2.Token, *oidc.IDToken, string, error) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
rawStoredState := sess.Values[sessionKeyLoginState]
|
||||||
|
receivedState := r.URL.Query().Get("state")
|
||||||
|
|
||||||
|
storedState, ok := rawStoredState.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, "", errors.New("could not find state in session")
|
||||||
|
}
|
||||||
|
|
||||||
|
if receivedState != storedState {
|
||||||
|
return nil, nil, "", errors.New("state mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
code := r.URL.Query().Get("code")
|
||||||
|
|
||||||
|
token, err := c.oauth2.Exchange(ctx, code)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, "", errors.Wrap(err, "could not exchange token")
|
||||||
|
}
|
||||||
|
|
||||||
|
rawIDToken, ok := token.Extra("id_token").(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, "", errors.New("could not find id token")
|
||||||
|
}
|
||||||
|
|
||||||
|
idToken, err := c.verifier.Verify(ctx, rawIDToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, "", errors.Wrap(err, "could not verify id token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, idToken, rawIDToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getRawIDToken(sess *sessions.Session) (string, error) {
|
||||||
|
rawIDToken, ok := sess.Values[sessionKeyIDToken].(string)
|
||||||
|
if !ok || rawIDToken == "" {
|
||||||
|
return "", errors.New("invalid id token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawIDToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getIDToken(r *http.Request, sess *sessions.Session) (*oidc.IDToken, error) {
|
||||||
|
rawIDToken, err := c.getRawIDToken(sess)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not retrieve raw idtoken")
|
||||||
|
}
|
||||||
|
|
||||||
|
idToken, err := c.verifier.Verify(r.Context(), rawIDToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not verify id token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return idToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(funcs ...ClientOptionFunc) *Client {
|
||||||
|
opts := NewClientOptions(funcs...)
|
||||||
|
|
||||||
|
oauth2 := &oauth2.Config{
|
||||||
|
ClientID: opts.ClientID,
|
||||||
|
ClientSecret: opts.ClientSecret,
|
||||||
|
Endpoint: opts.Provider.Endpoint(),
|
||||||
|
RedirectURL: opts.RedirectURL,
|
||||||
|
Scopes: opts.Scopes,
|
||||||
|
}
|
||||||
|
|
||||||
|
verifier := opts.Provider.Verifier(&oidc.Config{
|
||||||
|
ClientID: opts.ClientID,
|
||||||
|
SkipIssuerCheck: opts.SkipIssuerCheck,
|
||||||
|
})
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
oauth2: oauth2,
|
||||||
|
provider: opts.Provider,
|
||||||
|
verifier: verifier,
|
||||||
|
authParams: opts.AuthParams,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientOptions struct {
|
||||||
|
Provider *oidc.Provider
|
||||||
|
ClientID string
|
||||||
|
ClientSecret string
|
||||||
|
RedirectURL string
|
||||||
|
Scopes []string
|
||||||
|
AuthParams map[string]string
|
||||||
|
SkipIssuerCheck bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientOptionFunc func(*ClientOptions)
|
||||||
|
|
||||||
|
func WithRedirectURL(url string) ClientOptionFunc {
|
||||||
|
return func(opt *ClientOptions) {
|
||||||
|
opt.RedirectURL = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCredentials(clientID, clientSecret string) ClientOptionFunc {
|
||||||
|
return func(opt *ClientOptions) {
|
||||||
|
opt.ClientID = clientID
|
||||||
|
opt.ClientSecret = clientSecret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithScopes(scopes ...string) ClientOptionFunc {
|
||||||
|
return func(opt *ClientOptions) {
|
||||||
|
opt.Scopes = scopes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithAuthParams(params map[string]string) ClientOptionFunc {
|
||||||
|
return func(opt *ClientOptions) {
|
||||||
|
opt.AuthParams = params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSkipIssuerCheck(skip bool) ClientOptionFunc {
|
||||||
|
return func(opt *ClientOptions) {
|
||||||
|
opt.SkipIssuerCheck = skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProvider(ctx context.Context, issuer string, skipIssuerVerification bool) (*oidc.Provider, error) {
|
||||||
|
if skipIssuerVerification {
|
||||||
|
ctx = oidc.InsecureIssuerURLContext(ctx, issuer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return oidc.NewProvider(ctx, issuer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithProvider(provider *oidc.Provider) ClientOptionFunc {
|
||||||
|
return func(opt *ClientOptions) {
|
||||||
|
opt.Provider = provider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClientOptions(funcs ...ClientOptionFunc) *ClientOptions {
|
||||||
|
opt := &ClientOptions{
|
||||||
|
Scopes: []string{oidc.ScopeOpenID, "profile"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range funcs {
|
||||||
|
f(opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return opt
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
{
|
||||||
|
"$id": "https://forge.cadoles.com/cadoles/bouncer/schemas/authn-oidc-layer-options",
|
||||||
|
"title": "Options de configuration du layer 'authn-oidc'",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"oidc": {
|
||||||
|
"title": "Configuration du client OpenID Connect",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"clientId": {
|
||||||
|
"title": "Identifiant du client OpenID Connect",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"clientSecret": {
|
||||||
|
"title": "Secret du client OpenID Connect",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"issuerURL": {
|
||||||
|
"title": "URL de base du fournisseur OpenID Connect (racine du .well-known/openid-configuration)",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"postLogoutRedirectURL": {
|
||||||
|
"title": "URL de redirection après déconnexion",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"scopes": {
|
||||||
|
"title": "Scopes associés au client OpenID Connect",
|
||||||
|
"default": [
|
||||||
|
"openid"
|
||||||
|
],
|
||||||
|
"type": "array",
|
||||||
|
"item": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authParams": {
|
||||||
|
"title": "Paramètres d'URL supplémentaires à ajouter à la requête d'authentification OpenID Connect",
|
||||||
|
"default": {},
|
||||||
|
"description": "L'ensemble des clés valeurs renseignées seront transformées en variables d'URL lors de la requête d'authentification initiale. Permet par exemple d'ajouter les 'acr_values' requises par certains fournisseurs d'identité OpenID Connect.",
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
".*": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"loginCallbackPath": {
|
||||||
|
"title": "Chemin associé à l'URL de callback OpenID Connect",
|
||||||
|
"default": "/.bouncer/authn/oidc/%s/callback",
|
||||||
|
"description": "Le marqueur '%s' peut être utilisé pour injecter l'espace de nom '<proxy>/<layer>'.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"logoutPath": {
|
||||||
|
"title": "Chemin associé à l'URL de déconnexion",
|
||||||
|
"default": "/.bouncer/authn/oidc/%s/logout",
|
||||||
|
"description": "Le marqueur '%s' peut être utilisé pour injecter l'espace de nom '<proxy>/<layer>'.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"skipIssuerVerification": {
|
||||||
|
"title": "Activer/désactiver la vérification de concordance de l'identifiant du fournisseur d'identité",
|
||||||
|
"default": false,
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"clientId",
|
||||||
|
"clientSecret",
|
||||||
|
"issuerURL",
|
||||||
|
"postLogoutRedirectURL"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"cookie": {
|
||||||
|
"title": "Configuration du cookie porteur de la session utilisateur",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"title": "Nom du cookie",
|
||||||
|
"default": "",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"domain": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"sameSite": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"httpOnly": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"secure": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"maxAge": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"oidc"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
const LayerType store.LayerType = "authn-oidc"
|
||||||
|
|
||||||
|
func NewLayer(store sessions.Store) *authn.Layer {
|
||||||
|
return authn.NewLayer(LayerType, &Authenticator{store: store})
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultCookieName = "_bouncer_authn_oidc"
|
||||||
|
|
||||||
|
type LayerOptions struct {
|
||||||
|
authn.LayerOptions
|
||||||
|
OIDC OIDCOptions `mapstructure:"oidc"`
|
||||||
|
Cookie CookieOptions `mapstructure:"cookie"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OIDCOptions struct {
|
||||||
|
ClientID string `mapstructure:"clientId"`
|
||||||
|
ClientSecret string `mapstructure:"clientSecret"`
|
||||||
|
LoginCallbackPath string `mapstructure:"loginCallbackPath"`
|
||||||
|
LogoutPath string `mapstructure:"logoutPath"`
|
||||||
|
IssuerURL string `mapstructure:"issuerURL"`
|
||||||
|
SkipIssuerVerification bool `mapstructure:"skipIssuerVerification"`
|
||||||
|
PostLogoutRedirectURL string `mapstructure:"postLogoutRedirectURL"`
|
||||||
|
Scopes []string `mapstructure:"scopes"`
|
||||||
|
AuthParams map[string]string `mapstructure:"authParams"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CookieOptions struct {
|
||||||
|
Name string `mapstructure:"name"`
|
||||||
|
Domain string `mapstructure:"domain"`
|
||||||
|
Path string `mapstructure:"path"`
|
||||||
|
SameSite string `mapstructure:"sameSite"`
|
||||||
|
Secure bool `mapstructure:"secure"`
|
||||||
|
HTTPOnly bool `mapstructure:"httpOnly"`
|
||||||
|
MaxAge time.Duration `mapstructure:"maxAge"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) {
|
||||||
|
layerOptions := LayerOptions{
|
||||||
|
LayerOptions: authn.DefaultLayerOptions(),
|
||||||
|
OIDC: OIDCOptions{
|
||||||
|
LoginCallbackPath: "/.bouncer/authn/oidc/%s/callback",
|
||||||
|
LogoutPath: "/.bouncer/authn/oidc/%s/logout",
|
||||||
|
Scopes: []string{"openid"},
|
||||||
|
},
|
||||||
|
Cookie: CookieOptions{
|
||||||
|
Name: defaultCookieName,
|
||||||
|
Path: "/",
|
||||||
|
HTTPOnly: true,
|
||||||
|
MaxAge: time.Hour,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := authn.FromStoreOptions(storeOptions, &layerOptions); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &layerOptions, nil
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed layer-options.json
|
||||||
|
var RawLayerOptionsSchema []byte
|
|
@ -0,0 +1,8 @@
|
||||||
|
package authn
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed layer-options.json
|
||||||
|
var RawLayerOptionsSchema []byte
|
|
@ -0,0 +1,17 @@
|
||||||
|
package authn
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Subject string `json:"subject" expr:"subject"`
|
||||||
|
Attrs map[string]any `json:"attrs" expr:"attrs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUser(subject string, attrs map[string]any) *User {
|
||||||
|
if attrs == nil {
|
||||||
|
attrs = make(map[string]any)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &User{
|
||||||
|
Subject: subject,
|
||||||
|
Attrs: attrs,
|
||||||
|
}
|
||||||
|
}
|
|
@ -254,7 +254,7 @@ func (q *Queue) updateMetrics(ctx context.Context, proxyName store.ProxyName, la
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queue) getCookieName(layerName store.LayerName) string {
|
func (q *Queue) getCookieName(layerName store.LayerName) string {
|
||||||
return fmt.Sprintf("_%s_%s", LayerType, layerName)
|
return fmt.Sprintf("_bouncer_%s_%s", LayerType, layerName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(adapter Adapter, funcs ...OptionFunc) *Queue {
|
func New(adapter Adapter, funcs ...OptionFunc) *Queue {
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Extend(base []byte, schema []byte) ([]byte, error) {
|
||||||
|
var (
|
||||||
|
extension map[string]any
|
||||||
|
extended map[string]any
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := json.Unmarshal(base, &extended); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(schema, &extension); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
extended["$id"] = extension["$id"]
|
||||||
|
extended["title"] = extension["title"]
|
||||||
|
|
||||||
|
props := extension["properties"].(map[string]any)
|
||||||
|
extendedProps := extended["properties"].(map[string]any)
|
||||||
|
for key, val := range props {
|
||||||
|
extendedProps[key] = val
|
||||||
|
}
|
||||||
|
extended["properties"] = extendedProps
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(extended, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/session"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StoreAdapter struct {
|
||||||
|
client redis.UniversalClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// Del implements authn.StoreAdapter.
|
||||||
|
func (s *StoreAdapter) Del(ctx context.Context, key string) error {
|
||||||
|
if err := s.client.Del(ctx, key).Err(); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get implements authn.StoreAdapter.
|
||||||
|
func (s *StoreAdapter) Get(ctx context.Context, key string) ([]byte, error) {
|
||||||
|
cmd := s.client.Get(ctx, key)
|
||||||
|
|
||||||
|
if err := cmd.Err(); err != nil {
|
||||||
|
if errors.Is(err, redis.Nil) {
|
||||||
|
return nil, errors.WithStack(session.ErrNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := cmd.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set implements authn.StoreAdapter.
|
||||||
|
func (s *StoreAdapter) Set(ctx context.Context, key string, data []byte, ttl time.Duration) error {
|
||||||
|
if err := s.client.Set(ctx, key, data, ttl).Err(); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStoreAdapter(client redis.UniversalClient) *StoreAdapter {
|
||||||
|
return &StoreAdapter{
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ session.StoreAdapter = &StoreAdapter{}
|
||||||
|
)
|
|
@ -0,0 +1,47 @@
|
||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
Session sessions.Options
|
||||||
|
KeyPrefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
type OptionFunc func(opts *Options)
|
||||||
|
|
||||||
|
func NewOptions(funcs ...OptionFunc) *Options {
|
||||||
|
opts := &Options{
|
||||||
|
Session: sessions.Options{
|
||||||
|
Path: "/",
|
||||||
|
Domain: "",
|
||||||
|
MaxAge: int(time.Hour.Seconds()),
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: false,
|
||||||
|
SameSite: http.SameSiteDefaultMode,
|
||||||
|
},
|
||||||
|
KeyPrefix: "session:",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fn := range funcs {
|
||||||
|
fn(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSessionOptions(options sessions.Options) OptionFunc {
|
||||||
|
return func(opts *Options) {
|
||||||
|
opts.Session = options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithKeyPrefix(prefix string) OptionFunc {
|
||||||
|
return func(opts *Options) {
|
||||||
|
opts.KeyPrefix = prefix
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,182 @@
|
||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base32"
|
||||||
|
"encoding/gob"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotFound = errors.New("not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
type StoreAdapter interface {
|
||||||
|
Set(ctx context.Context, key string, data []byte, ttl time.Duration) error
|
||||||
|
Del(ctx context.Context, key string) error
|
||||||
|
Get(ctx context.Context, key string) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
adapter StoreAdapter
|
||||||
|
options sessions.Options
|
||||||
|
keyPrefix string
|
||||||
|
keyGen KeyGenFunc
|
||||||
|
serializer SessionSerializer
|
||||||
|
}
|
||||||
|
|
||||||
|
type KeyGenFunc func() (string, error)
|
||||||
|
|
||||||
|
func NewStore(adapter StoreAdapter, funcs ...OptionFunc) *Store {
|
||||||
|
opts := NewOptions(funcs...)
|
||||||
|
rs := &Store{
|
||||||
|
options: opts.Session,
|
||||||
|
adapter: adapter,
|
||||||
|
keyPrefix: opts.KeyPrefix,
|
||||||
|
keyGen: generateRandomKey,
|
||||||
|
serializer: GobSerializer{},
|
||||||
|
}
|
||||||
|
|
||||||
|
return rs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Get(r *http.Request, name string) (*sessions.Session, error) {
|
||||||
|
return sessions.GetRegistry(r).Get(s, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) New(r *http.Request, name string) (*sessions.Session, error) {
|
||||||
|
session := sessions.NewSession(s, name)
|
||||||
|
opts := s.options
|
||||||
|
session.Options = &opts
|
||||||
|
session.IsNew = true
|
||||||
|
|
||||||
|
c, err := r.Cookie(name)
|
||||||
|
if err != nil {
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
session.ID = c.Value
|
||||||
|
|
||||||
|
err = s.load(r.Context(), session)
|
||||||
|
if err == nil {
|
||||||
|
session.IsNew = false
|
||||||
|
} else if !errors.Is(err, ErrNotFound) {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
|
||||||
|
if session.Options.MaxAge <= 0 {
|
||||||
|
if err := s.delete(r.Context(), session); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.ID == "" {
|
||||||
|
id, err := s.keyGen()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to generate session id")
|
||||||
|
}
|
||||||
|
session.ID = id
|
||||||
|
}
|
||||||
|
if err := s.save(r.Context(), session); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
http.SetCookie(w, sessions.NewCookie(session.Name(), session.ID, session.Options))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Options(opts sessions.Options) {
|
||||||
|
s.options = opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) KeyPrefix(keyPrefix string) {
|
||||||
|
s.keyPrefix = keyPrefix
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) KeyGen(f KeyGenFunc) {
|
||||||
|
s.keyGen = f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Serializer(ss SessionSerializer) {
|
||||||
|
s.serializer = ss
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) save(ctx context.Context, session *sessions.Session) error {
|
||||||
|
b, err := s.serializer.Serialize(session)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.adapter.Set(ctx, s.keyPrefix+session.ID, b, time.Duration(session.Options.MaxAge)*time.Second); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// load reads session from Redis
|
||||||
|
func (s *Store) load(ctx context.Context, session *sessions.Session) error {
|
||||||
|
data, err := s.adapter.Get(ctx, s.keyPrefix+session.ID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.serializer.Deserialize(data, session)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete deletes session in Redis
|
||||||
|
func (s *Store) delete(ctx context.Context, session *sessions.Session) error {
|
||||||
|
if err := s.adapter.Del(ctx, s.keyPrefix+session.ID); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionSerializer provides an interface for serialize/deserialize a session
|
||||||
|
type SessionSerializer interface {
|
||||||
|
Serialize(s *sessions.Session) ([]byte, error)
|
||||||
|
Deserialize(b []byte, s *sessions.Session) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gob serializer
|
||||||
|
type GobSerializer struct{}
|
||||||
|
|
||||||
|
func (gs GobSerializer) Serialize(s *sessions.Session) ([]byte, error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
enc := gob.NewEncoder(buf)
|
||||||
|
|
||||||
|
if err := enc.Encode(s.Values); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gs GobSerializer) Deserialize(d []byte, s *sessions.Session) error {
|
||||||
|
dec := gob.NewDecoder(bytes.NewBuffer(d))
|
||||||
|
return dec.Decode(&s.Values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateRandomKey returns a new random key
|
||||||
|
func generateRandomKey() (string, error) {
|
||||||
|
k := make([]byte, 64)
|
||||||
|
if _, err := io.ReadFull(rand.Reader, k); err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
return strings.TrimRight(base32.StdEncoding.EncodeToString(k), "="), nil
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package setup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/authn/oidc"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/schema"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/session"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/session/adapter/redis"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
extended, err := schema.Extend(authn.RawLayerOptionsSchema, oidc.RawLayerOptionsSchema)
|
||||||
|
if err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not extend authn base layer options schema"))
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisterLayer(oidc.LayerType, setupAuthnOIDCLayer, extended)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupAuthnOIDCLayer(conf *config.Config) (director.Layer, error) {
|
||||||
|
rdb := newRedisClient(conf.Redis)
|
||||||
|
adapter := redis.NewStoreAdapter(rdb)
|
||||||
|
store := session.NewStore(adapter)
|
||||||
|
|
||||||
|
return oidc.NewLayer(store), nil
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
from: ["*"]
|
||||||
|
to: http://localhost:8082
|
||||||
|
enabled: true
|
||||||
|
weight: 0
|
||||||
|
recreate: true
|
||||||
|
layers:
|
||||||
|
my-queue:
|
||||||
|
type: queue
|
||||||
|
enabled: true
|
||||||
|
weight: 100
|
||||||
|
options:
|
||||||
|
capacity: 100
|
||||||
|
# oidc:
|
||||||
|
# type: authn-oidc
|
||||||
|
# enabled: true
|
||||||
|
# weight: 0
|
||||||
|
# options:
|
||||||
|
# oidc:
|
||||||
|
# clientId: my-client-id
|
||||||
|
# clientSecret: my-client-id
|
||||||
|
# issuerURL: https://forge.cadoles.com/
|
||||||
|
# postLogoutRedirectURL: http://localhost:8080
|
||||||
|
# scopes: ["profile", "openid", "email"]
|
||||||
|
# authParams:
|
||||||
|
# acr_values: "eidas2"
|
||||||
|
# cookie:
|
||||||
|
# maxAge: 60m
|
36
modd.conf
36
modd.conf
|
@ -1,17 +1,3 @@
|
||||||
**/*.go
|
|
||||||
internal/**/*.json
|
|
||||||
modd.conf
|
|
||||||
config.yml
|
|
||||||
layers/**
|
|
||||||
.env {
|
|
||||||
prep: make RUN_INSTALL_TESTS=no GOTEST_ARGS="-short" test
|
|
||||||
prep: make build-bouncer
|
|
||||||
prep: make config.yml
|
|
||||||
prep: make .bouncer-token
|
|
||||||
daemon: make run BOUNCER_CMD="--debug --config config.yml server admin run"
|
|
||||||
daemon: make run BOUNCER_CMD="--debug --config config.yml server proxy run"
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
daemon +sigint: make run-redis
|
daemon +sigint: make run-redis
|
||||||
}
|
}
|
||||||
|
@ -20,3 +6,25 @@ misc/prometheus/prometheus.yml
|
||||||
{
|
{
|
||||||
daemon +sigint: make run-prometheus
|
daemon +sigint: make run-prometheus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
./cmd/dummy/*.go
|
||||||
|
./cmd/dummy/*.gohtml
|
||||||
|
{
|
||||||
|
daemon: go run ./cmd/dummy
|
||||||
|
}
|
||||||
|
|
||||||
|
**/*.go
|
||||||
|
internal/**/*.json
|
||||||
|
modd.conf
|
||||||
|
config.yml
|
||||||
|
layers/**
|
||||||
|
data/bootstrap.d/*.yml
|
||||||
|
.env {
|
||||||
|
prep: make RUN_INSTALL_TESTS=no GOTEST_ARGS="-short" test
|
||||||
|
prep: make build-bouncer
|
||||||
|
prep: make config.yml
|
||||||
|
prep: make .bouncer-token
|
||||||
|
daemon: make run BOUNCER_CMD="--debug --config config.yml server admin run"
|
||||||
|
daemon: make run BOUNCER_CMD="--debug --config config.yml server proxy run"
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue