17 changed files with 23 additions and 1163 deletions
@ -1,301 +0,0 @@ |
|||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. |
|||
|
|||
|
|||
[[projects]] |
|||
digest = "1:efcf35206d596370b758dc4dcc16ce9a5a20792992dd9bcf5090963895dc21a8" |
|||
name = "github.com/cloudtrust/common-service" |
|||
packages = ["errors"] |
|||
pruneopts = "" |
|||
revision = "0da35eaf6d16a949088ce7f68ac7ae43a3015847" |
|||
version = "v2.3.2" |
|||
|
|||
[[projects]] |
|||
digest = "1:bb7f91ab4d1c44a3bb2651c613463c134165bda0282fca891a63b88d1b501997" |
|||
name = "github.com/coreos/go-oidc" |
|||
packages = ["."] |
|||
pruneopts = "" |
|||
revision = "8d771559cf6e5111c9b9159810d0e4538e7cdc82" |
|||
version = "v2.2.1" |
|||
|
|||
[[projects]] |
|||
digest = "1:0deddd908b6b4b768cfc272c16ee61e7088a60f7fe2f06c547bd3d8e1f8b8e77" |
|||
name = "github.com/davecgh/go-spew" |
|||
packages = ["spew"] |
|||
pruneopts = "" |
|||
revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" |
|||
version = "v1.1.1" |
|||
|
|||
[[projects]] |
|||
digest = "1:0c07a9cb3d3c845439a4fcaae6c8bdd0e7727cbbd3acf1e032e5d4a2dc132306" |
|||
name = "github.com/gbrlsnchs/jwt" |
|||
packages = ["."] |
|||
pruneopts = "" |
|||
revision = "808efa0714baff8c25cc65ef8681966740beb9f9" |
|||
version = "v2.0.0" |
|||
|
|||
[[projects]] |
|||
digest = "1:324437ddda77526fea02c29a2208ef1260e7a96ff0202965143215941564c400" |
|||
name = "github.com/go-kit/kit" |
|||
packages = [ |
|||
"endpoint", |
|||
"log", |
|||
"transport", |
|||
"transport/http", |
|||
] |
|||
pruneopts = "" |
|||
revision = "cc938d52e0cdf4c811ab203f428fcd06f9d9a148" |
|||
version = "v0.10.0" |
|||
|
|||
[[projects]] |
|||
digest = "1:aa9a6ccd5fd7d29804a27cb0666bc4ac5eb4b73439b7edd54f4b377e5ef8bb47" |
|||
name = "github.com/go-logfmt/logfmt" |
|||
packages = ["."] |
|||
pruneopts = "" |
|||
revision = "3be5f6aae7db841d31dc5e1b3bb7b4cff19200b3" |
|||
version = "v0.5.0" |
|||
|
|||
[[projects]] |
|||
digest = "1:bc24d32292c699c0cc375ae09fef8340c37360f46ec60cad98312b699c039422" |
|||
name = "github.com/golang/mock" |
|||
packages = ["gomock"] |
|||
pruneopts = "" |
|||
revision = "f7b1909c82a8958747e5c87c6a5c3b2eaed8a33d" |
|||
version = "v1.4.4" |
|||
|
|||
[[projects]] |
|||
digest = "1:6532affeeaaccdc6919d5773516176b77de02b4af8cf9a7fac16bae77aa319c5" |
|||
name = "github.com/golang/protobuf" |
|||
packages = ["proto"] |
|||
pruneopts = "" |
|||
revision = "4846b58453b3708320bdb524f25cc5a1d9cda4d4" |
|||
version = "v1.4.3" |
|||
|
|||
[[projects]] |
|||
digest = "1:20e3b61797cba6a42b110320b703a0b86e2771f324411e75557c2acbc819b1b5" |
|||
name = "github.com/gorilla/mux" |
|||
packages = ["."] |
|||
pruneopts = "" |
|||
revision = "98cb6bf42e086f6af920b965c38cacc07402d51b" |
|||
version = "v1.8.0" |
|||
|
|||
[[projects]] |
|||
digest = "1:1d7e1867c49a6dd9856598ef7c3123604ea3daabf5b83f303ff457bcbc410b1d" |
|||
name = "github.com/pkg/errors" |
|||
packages = ["."] |
|||
pruneopts = "" |
|||
revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4" |
|||
version = "v0.8.1" |
|||
|
|||
[[projects]] |
|||
digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411" |
|||
name = "github.com/pmezard/go-difflib" |
|||
packages = ["difflib"] |
|||
pruneopts = "" |
|||
revision = "792786c7400a136282c1664665ae0a8db921c6c2" |
|||
version = "v1.0.0" |
|||
|
|||
[[projects]] |
|||
branch = "master" |
|||
digest = "1:3957a6d09f51e1efa16c86156e22521402e6c13867213fd45fe5a37508013021" |
|||
name = "github.com/pquerna/cachecontrol" |
|||
packages = [ |
|||
".", |
|||
"cacheobject", |
|||
] |
|||
pruneopts = "" |
|||
revision = "ac21108117ac345f5739431ee1e9a5fd7f82ae1c" |
|||
|
|||
[[projects]] |
|||
digest = "1:688428eeb1ca80d92599eb3254bdf91b51d7e232fead3a73844c1f201a281e51" |
|||
name = "github.com/spf13/pflag" |
|||
packages = ["."] |
|||
pruneopts = "" |
|||
revision = "2e9d26c8c37aae03e3f9d4e90b7116f5accb7cab" |
|||
version = "v1.0.5" |
|||
|
|||
[[projects]] |
|||
digest = "1:83fd2513b9f6ae0997bf646db6b74e9e00131e31002116fda597175f25add42d" |
|||
name = "github.com/stretchr/testify" |
|||
packages = ["assert"] |
|||
pruneopts = "" |
|||
revision = "f654a9112bbeac49ca2cd45bfbe11533c4666cf8" |
|||
version = "v1.6.1" |
|||
|
|||
[[projects]] |
|||
branch = "master" |
|||
digest = "1:22902e9ca5ef0028bb8e6a3dacb884d166d0259841a15288d81ddb5d61fa68c0" |
|||
name = "golang.org/x/crypto" |
|||
packages = [ |
|||
"ed25519", |
|||
"ed25519/internal/edwards25519", |
|||
"pbkdf2", |
|||
] |
|||
pruneopts = "" |
|||
revision = "eec23a3978adcfd26c29f4153eaa3e3d9b2cc53a" |
|||
|
|||
[[projects]] |
|||
branch = "master" |
|||
digest = "1:87515e47f85690ee710603c4de5c631d034f203799768278e43ec8497078b47e" |
|||
name = "golang.org/x/net" |
|||
packages = [ |
|||
"context", |
|||
"context/ctxhttp", |
|||
"idna", |
|||
"publicsuffix", |
|||
] |
|||
pruneopts = "" |
|||
revision = "6772e930b67bb09bf22262c7378e7d2f67cf59d1" |
|||
|
|||
[[projects]] |
|||
branch = "master" |
|||
digest = "1:bf98155c68bf5f703fd3cd79f8a2cf4372460bf4de00ef0aeadbade1befc3c4c" |
|||
name = "golang.org/x/oauth2" |
|||
packages = [ |
|||
".", |
|||
"internal", |
|||
] |
|||
pruneopts = "" |
|||
revision = "01de73cf58bdca33ccc181d1bd6d63ebcf21ccca" |
|||
|
|||
[[projects]] |
|||
digest = "1:4101e2977ebdbb93018565cd1c03db0151143b303a7f7a171592d3160f1498b2" |
|||
name = "golang.org/x/text" |
|||
packages = [ |
|||
"collate", |
|||
"collate/build", |
|||
"internal/colltab", |
|||
"internal/gen", |
|||
"internal/language", |
|||
"internal/language/compact", |
|||
"internal/tag", |
|||
"internal/triegen", |
|||
"internal/ucd", |
|||
"language", |
|||
"secure/bidirule", |
|||
"transform", |
|||
"unicode/bidi", |
|||
"unicode/cldr", |
|||
"unicode/norm", |
|||
"unicode/rangetable", |
|||
] |
|||
pruneopts = "" |
|||
revision = "75a595aef632b07c6eeaaa805adb6f0f66e4130e" |
|||
version = "v0.3.5" |
|||
|
|||
[[projects]] |
|||
digest = "1:13ee408d4cf721c5b576dbaff8d88ced35aadc9675274cc6b4ad009ac0372955" |
|||
name = "google.golang.org/appengine" |
|||
packages = [ |
|||
"internal", |
|||
"internal/base", |
|||
"internal/datastore", |
|||
"internal/log", |
|||
"internal/remote_api", |
|||
"internal/urlfetch", |
|||
"urlfetch", |
|||
] |
|||
pruneopts = "" |
|||
revision = "5d1c1d03f8703c2e81478d9a30e9afa2d3e4bd8a" |
|||
version = "v1.6.7" |
|||
|
|||
[[projects]] |
|||
digest = "1:0d049ff01749ce1d6092c85cb90a02dfdb3b401939b13415b7e608f36bc8ecee" |
|||
name = "google.golang.org/protobuf" |
|||
packages = [ |
|||
"encoding/prototext", |
|||
"encoding/protowire", |
|||
"internal/descfmt", |
|||
"internal/descopts", |
|||
"internal/detrand", |
|||
"internal/encoding/defval", |
|||
"internal/encoding/messageset", |
|||
"internal/encoding/tag", |
|||
"internal/encoding/text", |
|||
"internal/errors", |
|||
"internal/fieldsort", |
|||
"internal/filedesc", |
|||
"internal/filetype", |
|||
"internal/flags", |
|||
"internal/genid", |
|||
"internal/impl", |
|||
"internal/mapsort", |
|||
"internal/pragma", |
|||
"internal/set", |
|||
"internal/strs", |
|||
"internal/version", |
|||
"proto", |
|||
"reflect/protoreflect", |
|||
"reflect/protoregistry", |
|||
"runtime/protoiface", |
|||
"runtime/protoimpl", |
|||
] |
|||
pruneopts = "" |
|||
revision = "3f7a61f89bb6813f89d981d1870ed68da0b3c3f1" |
|||
version = "v1.25.0" |
|||
|
|||
[[projects]] |
|||
digest = "1:95fa5eae3b22887e8aea55ad4f93bc1374d586f7dd3504cf0010845ccc0a95a8" |
|||
name = "gopkg.in/h2non/gentleman.v2" |
|||
packages = [ |
|||
".", |
|||
"context", |
|||
"middleware", |
|||
"mux", |
|||
"plugin", |
|||
"plugins/body", |
|||
"plugins/bodytype", |
|||
"plugins/cookies", |
|||
"plugins/headers", |
|||
"plugins/multipart", |
|||
"plugins/query", |
|||
"plugins/timeout", |
|||
"plugins/url", |
|||
"utils", |
|||
] |
|||
pruneopts = "" |
|||
revision = "34f7caeaf69f4668a88f8294ec18665fd2756b84" |
|||
version = "v2.0.4" |
|||
|
|||
[[projects]] |
|||
digest = "1:7436f519828cd59ec8f6beac99e397a1bc9a4ce8871d9fbca029bddeaba48a92" |
|||
name = "gopkg.in/square/go-jose.v2" |
|||
packages = [ |
|||
".", |
|||
"cipher", |
|||
"json", |
|||
] |
|||
pruneopts = "" |
|||
revision = "3a5ee095dcb5030a9de84fb92c222ac652fff176" |
|||
version = "v2.5.1" |
|||
|
|||
[[projects]] |
|||
branch = "v3" |
|||
digest = "1:3f081584f03a869274b31afe33c22ce278c53767cb58963916746109e1d50535" |
|||
name = "gopkg.in/yaml.v3" |
|||
packages = ["."] |
|||
pruneopts = "" |
|||
revision = "496545a6307b2a7d7a710fd516e5e16e8ab62dbc" |
|||
|
|||
[solve-meta] |
|||
analyzer-name = "dep" |
|||
analyzer-version = 1 |
|||
input-imports = [ |
|||
"github.com/cloudtrust/common-service/errors", |
|||
"github.com/coreos/go-oidc", |
|||
"github.com/gbrlsnchs/jwt", |
|||
"github.com/go-kit/kit/transport/http", |
|||
"github.com/golang/mock/gomock", |
|||
"github.com/gorilla/mux", |
|||
"github.com/pkg/errors", |
|||
"github.com/spf13/pflag", |
|||
"github.com/stretchr/testify/assert", |
|||
"gopkg.in/h2non/gentleman.v2", |
|||
"gopkg.in/h2non/gentleman.v2/plugin", |
|||
"gopkg.in/h2non/gentleman.v2/plugins/body", |
|||
"gopkg.in/h2non/gentleman.v2/plugins/headers", |
|||
"gopkg.in/h2non/gentleman.v2/plugins/query", |
|||
"gopkg.in/h2non/gentleman.v2/plugins/timeout", |
|||
"gopkg.in/h2non/gentleman.v2/plugins/url", |
|||
] |
|||
solver-name = "gps-cdcl" |
|||
solver-version = 1 |
|||
@ -1,37 +0,0 @@ |
|||
|
|||
# Gopkg.toml example |
|||
# |
|||
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md |
|||
# for detailed Gopkg.toml documentation. |
|||
# |
|||
# required = ["github.com/user/thing/cmd/thing"] |
|||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] |
|||
# |
|||
# [[constraint]] |
|||
# name = "github.com/user/project" |
|||
# version = "1.0.0" |
|||
# |
|||
# [[constraint]] |
|||
# name = "github.com/user/project2" |
|||
# branch = "dev" |
|||
# source = "github.com/myfork/project2" |
|||
# |
|||
# [[override]] |
|||
# name = "github.com/x/y" |
|||
# version = "2.4.0" |
|||
|
|||
[[constraint]] |
|||
name = "github.com/cloudtrust/common-service" |
|||
version = "2.3.2" |
|||
|
|||
[[constraint]] |
|||
name = "github.com/pkg/errors" |
|||
version = "0.8.0" |
|||
|
|||
[[constraint]] |
|||
name = "gopkg.in/h2non/gentleman.v2" |
|||
version = "2.0.0" |
|||
|
|||
[[constraint]] |
|||
name = "github.com/gbrlsnchs/jwt" |
|||
version = "2.0.0" |
|||
@ -1,67 +0,0 @@ |
|||
pipeline { |
|||
agent any |
|||
options { |
|||
timestamps() |
|||
timeout(time: 3600, unit: 'SECONDS') |
|||
} |
|||
environment{ |
|||
BUILD_PATH="/home/jenkins/gopath/src/github.com/cloudtrust/keycloak-client" |
|||
} |
|||
stages { |
|||
stage('Build') { |
|||
agent { |
|||
label 'jenkins-slave-go-ct' |
|||
} |
|||
steps { |
|||
script { |
|||
sh 'printenv' |
|||
def isBranch = "" |
|||
if (!env.CHANGE_ID) { |
|||
isBranch = " || true" |
|||
} |
|||
withCredentials([usernamePassword(credentialsId: 'cloudtrust-cicd-sonarqube', usernameVariable: 'USER', passwordVariable: 'PASS')]) { |
|||
sh """ |
|||
set -eo pipefail |
|||
|
|||
mkdir -p "${BUILD_PATH}" |
|||
cp -r "${WORKSPACE}/." "${BUILD_PATH}/" |
|||
cd "${BUILD_PATH}" |
|||
|
|||
golint ./... | tee golint.out || true |
|||
|
|||
dep ensure |
|||
|
|||
go generate ./... |
|||
|
|||
go test -coverprofile=coverage.out -json ./... | tee report.json |
|||
go tool cover -func=coverage.out |
|||
bash -c \"go vet ./... > >(cat) 2> >(tee govet.out)\" || true |
|||
gometalinter --vendor --disable=gotype --disable=golint --disable=vet --disable=gocyclo --exclude=/usr/local/go/src --deadline=300s ./... | tee gometalinter.out || true |
|||
|
|||
nancy -no-color Gopkg.lock || true |
|||
|
|||
JAVA_TOOL_OPTIONS="" sonar-scanner \ |
|||
-Dsonar.host.url=https://sonarqube-cloudtrust-cicd.openshift.west.ch.elca-cloud.com \ |
|||
-Dsonar.login="${USER}" \ |
|||
-Dsonar.password="${PASS}" \ |
|||
-Dsonar.sourceEncoding=UTF-8 \ |
|||
-Dsonar.projectKey=keycloak-client \ |
|||
-Dsonar.projectName=keycloak-client \ |
|||
-Dsonar.projectVersion="${env.GIT_COMMIT}" \ |
|||
-Dsonar.sources=. \ |
|||
-Dsonar.exclusions=**/*_test.go,**/vendor/**,**/mock/** \ |
|||
-Dsonar.tests=. \ |
|||
-Dsonar.test.inclusions=**/*_test.go \ |
|||
-Dsonar.test.exclusions=**/vendor/** \ |
|||
-Dsonar.go.coverage.reportPaths=./coverage.out \ |
|||
-Dsonar.go.tests.reportPaths=./report.json \ |
|||
-Dsonar.go.govet.reportPaths=./govet.out \ |
|||
-Dsonar.go.golint.reportPaths=./golint.out \ |
|||
-Dsonar.go.gometalinter.reportPaths=./gometalinter.out ${isBranch} |
|||
""" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,107 +0,0 @@ |
|||
package api |
|||
|
|||
import ( |
|||
"github.com/cloudtrust/keycloak-client/v3" |
|||
"gopkg.in/h2non/gentleman.v2/plugin" |
|||
"gopkg.in/h2non/gentleman.v2/plugins/body" |
|||
"gopkg.in/h2non/gentleman.v2/plugins/headers" |
|||
"gopkg.in/h2non/gentleman.v2/plugins/query" |
|||
"gopkg.in/h2non/gentleman.v2/plugins/url" |
|||
) |
|||
|
|||
const ( |
|||
accountExtensionAPIPath = "/auth/realms/master/api/account/realms/:realm" |
|||
accountExecuteActionsEmail = accountExtensionAPIPath + "/execute-actions-email" |
|||
accountSendEmail = accountExtensionAPIPath + "/send-email" |
|||
accountCredentialsPath = accountExtensionAPIPath + "/credentials" |
|||
accountPasswordPath = accountCredentialsPath + "/password" |
|||
accountCredentialsRegistratorsPath = accountCredentialsPath + "/registrators" |
|||
accountCredentialIDPath = accountCredentialsPath + "/:credentialID" |
|||
accountCredentialLabelPath = accountCredentialIDPath + "/label" |
|||
accountMoveFirstPath = accountCredentialIDPath + "/moveToFirst" |
|||
accountMoveAfterPath = accountCredentialIDPath + "/moveAfter/:previousCredentialID" |
|||
) |
|||
|
|||
var ( |
|||
hdrAcceptJSON = headers.Set("Accept", "application/json") |
|||
hdrContentTypeTextPlain = headers.Set("Content-Type", "text/plain") |
|||
) |
|||
|
|||
// GetCredentials returns the list of credentials of the user
|
|||
func (c *AccountClient) GetCredentials(accessToken string, realmName string) ([]keycloak.CredentialRepresentation, error) { |
|||
var resp = []keycloak.CredentialRepresentation{} |
|||
var err = c.client.get(accessToken, &resp, url.Path(accountCredentialsPath), url.Param("realm", realmName), hdrAcceptJSON) |
|||
return resp, err |
|||
} |
|||
|
|||
// GetCredentialRegistrators returns list of credentials types available for the user
|
|||
func (c *AccountClient) GetCredentialRegistrators(accessToken string, realmName string) ([]string, error) { |
|||
var resp = []string{} |
|||
var err = c.client.get(accessToken, &resp, url.Path(accountCredentialsRegistratorsPath), url.Param("realm", realmName), hdrAcceptJSON) |
|||
return resp, err |
|||
} |
|||
|
|||
// UpdateLabelCredential updates the label of credential
|
|||
func (c *AccountClient) UpdateLabelCredential(accessToken string, realmName string, credentialID string, label string) error { |
|||
return c.client.put(accessToken, url.Path(accountCredentialLabelPath), url.Param("realm", realmName), url.Param("credentialID", credentialID), body.String(label), hdrAcceptJSON, hdrContentTypeTextPlain) |
|||
} |
|||
|
|||
// DeleteCredential deletes the credential
|
|||
func (c *AccountClient) DeleteCredential(accessToken string, realmName string, credentialID string) error { |
|||
return c.client.delete(accessToken, url.Path(accountCredentialIDPath), url.Param("realm", realmName), url.Param("credentialID", credentialID), hdrAcceptJSON) |
|||
} |
|||
|
|||
// MoveToFirst moves the credential at the top of the list
|
|||
func (c *AccountClient) MoveToFirst(accessToken string, realmName string, credentialID string) error { |
|||
_, err := c.client.post(accessToken, nil, url.Path(accountMoveFirstPath), url.Param("realm", realmName), url.Param("credentialID", credentialID), hdrAcceptJSON) |
|||
return err |
|||
} |
|||
|
|||
// MoveAfter moves the credential after the specified one into the list
|
|||
func (c *AccountClient) MoveAfter(accessToken string, realmName string, credentialID string, previousCredentialID string) error { |
|||
_, err := c.client.post(accessToken, nil, url.Path(accountMoveAfterPath), url.Param("realm", realmName), url.Param("credentialID", credentialID), url.Param("previousCredentialID", previousCredentialID), hdrAcceptJSON) |
|||
return err |
|||
} |
|||
|
|||
// UpdatePassword updates the user's password
|
|||
// Parameters: realm, currentPassword, newPassword, confirmPassword
|
|||
func (c *AccountClient) UpdatePassword(accessToken, realm, currentPassword, newPassword, confirmPassword string) (string, error) { |
|||
var m = map[string]string{"currentPassword": currentPassword, "newPassword": newPassword, "confirmation": confirmPassword} |
|||
return c.client.post(accessToken, nil, url.Path(accountPasswordPath), url.Param("realm", realm), body.JSON(m)) |
|||
} |
|||
|
|||
// GetAccount provides the user's information
|
|||
func (c *AccountClient) GetAccount(accessToken string, realm string) (keycloak.UserRepresentation, error) { |
|||
var resp = keycloak.UserRepresentation{} |
|||
var err = c.client.get(accessToken, &resp, url.Path(accountExtensionAPIPath), url.Param("realm", realm), hdrAcceptJSON) |
|||
return resp, err |
|||
} |
|||
|
|||
// UpdateAccount updates the user's information
|
|||
func (c *AccountClient) UpdateAccount(accessToken string, realm string, user keycloak.UserRepresentation) error { |
|||
_, err := c.client.post(accessToken, nil, url.Path(accountExtensionAPIPath), url.Param("realm", realm), body.JSON(user)) |
|||
return err |
|||
} |
|||
|
|||
// DeleteAccount deletes current user
|
|||
func (c *AccountClient) DeleteAccount(accessToken string, realmName string) error { |
|||
return c.client.delete(accessToken, url.Path(accountExtensionAPIPath), url.Param("realm", realmName), hdrAcceptJSON) |
|||
} |
|||
|
|||
// ExecuteActionsEmail sends an email with required actions to the user
|
|||
func (c *AccountClient) ExecuteActionsEmail(accessToken string, realmName string, actions []string) error { |
|||
return c.client.put(accessToken, url.Path(accountExecuteActionsEmail), url.Param("realm", realmName), body.JSON(actions)) |
|||
} |
|||
|
|||
// SendEmail sends an email
|
|||
func (c *AccountClient) SendEmail(accessToken, realmName, template, subject string, recipient *string, attributes map[string]string) error { |
|||
var plugins []plugin.Plugin |
|||
plugins = append(plugins, url.Path(accountSendEmail), url.Param("realm", realmName)) |
|||
plugins = append(plugins, query.Add("template", template), query.Add("subject", subject)) |
|||
if recipient != nil && len(*recipient) >= 0 { |
|||
plugins = append(plugins, query.Add("recipient", *recipient)) |
|||
} |
|||
plugins = append(plugins, body.JSON(attributes)) |
|||
_, err := c.client.post(accessToken, nil, plugins...) |
|||
return err |
|||
} |
|||
Binary file not shown.
@ -1,66 +0,0 @@ |
|||
package toolbox |
|||
|
|||
import ( |
|||
"errors" |
|||
"net/url" |
|||
"regexp" |
|||
"strings" |
|||
"time" |
|||
|
|||
"github.com/cloudtrust/keycloak-client/v3" |
|||
) |
|||
|
|||
// IssuerManager provides URL according to a given context
|
|||
type IssuerManager interface { |
|||
GetOidcVerifierProvider(issuer string) (OidcVerifierProvider, error) |
|||
} |
|||
|
|||
type issuerManager struct { |
|||
domainToVerifier map[string]OidcVerifierProvider |
|||
} |
|||
|
|||
func getProtocolAndDomain(URL string) string { |
|||
var r = regexp.MustCompile(`^\w+:\/\/[^\/]+`) |
|||
var match = r.FindStringSubmatch(URL) |
|||
if match != nil { |
|||
return strings.ToLower(match[0]) |
|||
} |
|||
// Best effort: if not found return the whole input string
|
|||
return URL |
|||
} |
|||
|
|||
// NewIssuerManager creates a new URLProvider
|
|||
func NewIssuerManager(config keycloak.Config) (IssuerManager, error) { |
|||
URLs := config.AddrTokenProvider |
|||
// Use default values when clients are not initializing these values
|
|||
cacheTTL := config.CacheTTL |
|||
if cacheTTL == 0 { |
|||
cacheTTL = 15 * time.Minute |
|||
} |
|||
errTolerance := config.ErrorTolerance |
|||
if errTolerance == 0 { |
|||
errTolerance = time.Minute |
|||
} |
|||
|
|||
var domainToVerifier = make(map[string]OidcVerifierProvider) |
|||
|
|||
for _, value := range strings.Split(URLs, " ") { |
|||
uToken, err := url.Parse(value) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
verifier := NewVerifierCache(uToken, cacheTTL, errTolerance) |
|||
domainToVerifier[getProtocolAndDomain(value)] = verifier |
|||
} |
|||
return &issuerManager{ |
|||
domainToVerifier: domainToVerifier, |
|||
}, nil |
|||
} |
|||
|
|||
func (im *issuerManager) GetOidcVerifierProvider(issuer string) (OidcVerifierProvider, error) { |
|||
issuerDomain := getProtocolAndDomain(issuer) |
|||
if verifier, ok := im.domainToVerifier[issuerDomain]; ok { |
|||
return verifier, nil |
|||
} |
|||
return nil, errors.New("Unknown issuer") |
|||
} |
|||
@ -1,51 +0,0 @@ |
|||
package toolbox |
|||
|
|||
import ( |
|||
"fmt" |
|||
"testing" |
|||
|
|||
"github.com/cloudtrust/keycloak-client/v3" |
|||
"github.com/stretchr/testify/assert" |
|||
) |
|||
|
|||
type contextKey int |
|||
|
|||
const ( |
|||
keyContextIssuerDomain contextKey = iota |
|||
) |
|||
|
|||
func TestGetProtocolAndDomain(t *testing.T) { |
|||
var invalidURL = "not a valid URL" |
|||
assert.Equal(t, invalidURL, getProtocolAndDomain(invalidURL)) |
|||
assert.Equal(t, "https://elca.ch", getProtocolAndDomain("https://ELCA.CH/PATH/TO/TARGET")) |
|||
} |
|||
|
|||
func TestNewIssuerManager(t *testing.T) { |
|||
t.Run("Invalid URL", func(t *testing.T) { |
|||
_, err := NewIssuerManager(keycloak.Config{AddrTokenProvider: ":"}) |
|||
assert.NotNil(t, err) |
|||
}) |
|||
|
|||
defaultPath := "http://default.domain.com:5555" |
|||
myDomainPath := "http://my.domain.com/path/to/somewhere" |
|||
otherDomainPath := "http://other.domain.com:2120/" |
|||
allDomains := fmt.Sprintf("%s %s %s", defaultPath, myDomainPath, otherDomainPath) |
|||
|
|||
prov, err := NewIssuerManager(keycloak.Config{AddrTokenProvider: allDomains}) |
|||
assert.Nil(t, err) |
|||
assert.NotNil(t, prov) |
|||
|
|||
// No issuer provided with context
|
|||
issuerNoContext, _ := prov.GetOidcVerifierProvider("") |
|||
// Unrecognized issuer provided in context
|
|||
issuerDefault, _ := prov.GetOidcVerifierProvider("http://unknown.issuer.com/one/path") |
|||
// Case insensitive
|
|||
issuerMyDomain, _ := prov.GetOidcVerifierProvider("http://MY.DOMAIN.COM/issuer") |
|||
// Other domain
|
|||
issuerOtherDomain, _ := prov.GetOidcVerifierProvider("http://other.domain.com:2120/any/thing/here") |
|||
|
|||
assert.Equal(t, issuerNoContext, issuerDefault) |
|||
assert.NotEqual(t, issuerNoContext, issuerMyDomain) |
|||
assert.NotEqual(t, issuerNoContext, issuerOtherDomain) |
|||
assert.NotEqual(t, issuerMyDomain, issuerOtherDomain) |
|||
} |
|||
@ -1,8 +0,0 @@ |
|||
package toolbox |
|||
|
|||
import "context" |
|||
|
|||
// Logger interface for logging with level
|
|||
type Logger interface { |
|||
Warn(ctx context.Context, keyvals ...interface{}) |
|||
} |
|||
@ -1 +0,0 @@ |
|||
package mock |
|||
@ -1,3 +0,0 @@ |
|||
package toolbox |
|||
|
|||
//go:generate mockgen -destination=./mock/logger.go -package=mock -mock_names=Logger=Logger github.com/cloudtrust/keycloak-client/v3/toolbox Logger
|
|||
@ -1,102 +0,0 @@ |
|||
package toolbox |
|||
|
|||
import ( |
|||
"bytes" |
|||
"context" |
|||
"encoding/json" |
|||
"fmt" |
|||
"net/http" |
|||
"net/url" |
|||
"strings" |
|||
"time" |
|||
|
|||
errorhandler "github.com/cloudtrust/common-service/errors" |
|||
"github.com/cloudtrust/keycloak-client/v3" |
|||
) |
|||
|
|||
// OidcTokenProvider provides OIDC tokens
|
|||
type OidcTokenProvider interface { |
|||
ProvideToken(ctx context.Context) (string, error) |
|||
} |
|||
|
|||
type oidcToken struct { |
|||
AccessToken string `json:"access_token,omitempty"` |
|||
ExpiresIn int64 `json:"expires_in,omitempty"` |
|||
RefreshToken string `json:"refresh_token,omitempty"` |
|||
RefreshExpiresIn int64 `json:"refresh_expires_in,omitempty"` |
|||
TokenType string `json:"token_type,omitempty"` |
|||
NotBeforePolicy int `json:"not-before-policy,omitempty"` |
|||
SessionState string `json:"session_state,omitempty"` |
|||
Scope string `json:"scope,omitempty"` |
|||
} |
|||
|
|||
type oidcTokenProvider struct { |
|||
timeout time.Duration |
|||
tokenURL string |
|||
reqBody string |
|||
logger Logger |
|||
oidcToken oidcToken |
|||
validUntil int64 |
|||
} |
|||
|
|||
const ( |
|||
// Max processing delay: let's assume that the user of OidcTokenProvider will have a maximum of 5 seconds to use the provided OIDC token
|
|||
maxProcessingDelay = int64(5) |
|||
) |
|||
|
|||
// NewOidcTokenProvider creates an OidcTokenProvider
|
|||
func NewOidcTokenProvider(config keycloak.Config, realm, username, password, clientID string, logger Logger) OidcTokenProvider { |
|||
var urls = strings.Split(config.AddrTokenProvider, " ") |
|||
var keycloakPublicURL = urls[0] |
|||
|
|||
var tokenURL = fmt.Sprintf("%s/auth/realms/%s/protocol/openid-connect/token", keycloakPublicURL, realm) |
|||
// If needed, can add &client_secret={secret}
|
|||
var body = fmt.Sprintf("grant_type=password&client_id=%s&username=%s&password=%s", |
|||
url.QueryEscape(clientID), url.QueryEscape(username), url.QueryEscape(password)) |
|||
|
|||
return &oidcTokenProvider{ |
|||
timeout: config.Timeout, |
|||
tokenURL: tokenURL, |
|||
reqBody: body, |
|||
logger: logger, |
|||
} |
|||
} |
|||
|
|||
func (o *oidcTokenProvider) ProvideToken(ctx context.Context) (string, error) { |
|||
if o.validUntil+maxProcessingDelay > time.Now().Unix() { |
|||
return o.oidcToken.AccessToken, nil |
|||
} |
|||
|
|||
var mimeType = "application/x-www-form-urlencoded" |
|||
var httpClient = http.Client{ |
|||
Timeout: o.timeout, |
|||
} |
|||
var resp, err = httpClient.Post(o.tokenURL, mimeType, strings.NewReader(o.reqBody)) |
|||
if err != nil { |
|||
o.logger.Warn(ctx, "msg", err.Error()) |
|||
return "", errorhandler.CreateInternalServerError("unexpected.httpResponse") |
|||
} |
|||
if err == nil && resp.StatusCode == http.StatusUnauthorized { |
|||
o.logger.Warn(ctx, "msg", "Technical user credentials are invalid") |
|||
return "", errorhandler.Error{ |
|||
Status: http.StatusUnauthorized, |
|||
Message: errorhandler.GetEmitter() + ".unauthorized", |
|||
} |
|||
} |
|||
if resp.StatusCode >= 400 || resp.Body == http.NoBody || resp.Body == nil { |
|||
o.logger.Warn(ctx, "msg", fmt.Sprintf("Unexpected behavior: unexpected http status (%d) or response has no body", resp.StatusCode)) |
|||
return "", errorhandler.CreateInternalServerError("unexpected.httpResponse") |
|||
} |
|||
|
|||
buf := new(bytes.Buffer) |
|||
_, _ = buf.ReadFrom(resp.Body) |
|||
|
|||
err = json.Unmarshal(buf.Bytes(), &o.oidcToken) |
|||
if err != nil { |
|||
o.logger.Warn(ctx, "msg", fmt.Sprintf("Can't deserialize token. JSON: %s", buf.String())) |
|||
return "", errorhandler.CreateInternalServerError("unexpected.oidcToken") |
|||
} |
|||
o.validUntil = time.Now().Unix() + o.oidcToken.ExpiresIn |
|||
|
|||
return o.oidcToken.AccessToken, nil |
|||
} |
|||
@ -1,107 +0,0 @@ |
|||
package toolbox |
|||
|
|||
import ( |
|||
"context" |
|||
"encoding/json" |
|||
"fmt" |
|||
"net/http" |
|||
"net/http/httptest" |
|||
"testing" |
|||
"time" |
|||
|
|||
"github.com/golang/mock/gomock" |
|||
|
|||
"github.com/cloudtrust/keycloak-client/v3" |
|||
"github.com/cloudtrust/keycloak-client/v3/toolbox/mock" |
|||
"github.com/gorilla/mux" |
|||
"github.com/stretchr/testify/assert" |
|||
) |
|||
|
|||
type TestResponse struct { |
|||
StatusCode int |
|||
NoBody bool |
|||
ResponseBody string |
|||
} |
|||
|
|||
func (t *TestResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
|||
w.WriteHeader(t.StatusCode) |
|||
if !t.NoBody { |
|||
w.Write([]byte(t.ResponseBody)) |
|||
} |
|||
time.Sleep(20 * time.Millisecond) |
|||
} |
|||
|
|||
func TestCreateToken(t *testing.T) { |
|||
var mockCtrl = gomock.NewController(t) |
|||
defer mockCtrl.Finish() |
|||
|
|||
var mockLogger = mock.NewLogger(mockCtrl) |
|||
mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() |
|||
|
|||
var oidcToken = oidcToken{ |
|||
AccessToken: `eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJZRTUtNUpBb2NOcG5zeEpEaGRyYVdyWlVCZTBMR2xfanNILUtnb1EwWi1FIn0.eyJqdGkiOiIxYjJkZDY2NS01ZGE1LTRiMzAtODY0MS0wNWQ4ZTk0NTQ2ZWQiLCJleHAiOjE1NzkyMDQ0ODYsIm5iZiI6MCwiaWF0IjoxNTc5MTY4NDg2LCJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwODAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjpbInBhc3NmbG93LXJlYWxtIiwibXlfcmVhbG0tcmVhbG0iLCJDbG91ZHRydXN0LXJlYWxtIiwibWFzdGVyLXJlYWxtIiwiYWNjb3VudCJdLCJzdWIiOiI3OTU5MjhjMy03N2Y1LTRmMjQtOTI0NC02NzBkMGJmMDJhMmQiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhZG1pbi1jbGkiLCJhdXRoX3RpbWUiOjAsInNlc3Npb25fc3RhdGUiOiI5ZTQ5NDA1MS1kZGQ1LTRhODctYTczZC1hOWU5YjMwYmFlZGEiLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImNyZWF0ZS1yZWFsbSIsIm9mZmxpbmVfYWNjZXNzIiwiYWRtaW4iLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7InBhc3NmbG93LXJlYWxtIjp7InJvbGVzIjpbInZpZXctcmVhbG0iLCJ2aWV3LWlkZW50aXR5LXByb3ZpZGVycyIsIm1hbmFnZS1pZGVudGl0eS1wcm92aWRlcnMiLCJpbXBlcnNvbmF0aW9uIiwiY3JlYXRlLWNsaWVudCIsIm1hbmFnZS11c2VycyIsInF1ZXJ5LXJlYWxtcyIsInZpZXctYXV0aG9yaXphdGlvbiIsInF1ZXJ5LWNsaWVudHMiLCJxdWVyeS11c2VycyIsIm1hbmFnZS1ldmVudHMiLCJtYW5hZ2UtcmVhbG0iLCJ2aWV3LWV2ZW50cyIsInZpZXctdXNlcnMiLCJ2aWV3LWNsaWVudHMiLCJtYW5hZ2UtYXV0aG9yaXphdGlvbiIsIm1hbmFnZS1jbGllbnRzIiwicXVlcnktZ3JvdXBzIl19LCJteV9yZWFsbS1yZWFsbSI6eyJyb2xlcyI6WyJ2aWV3LXJlYWxtIiwidmlldy1pZGVudGl0eS1wcm92aWRlcnMiLCJtYW5hZ2UtaWRlbnRpdHktcHJvdmlkZXJzIiwiaW1wZXJzb25hdGlvbiIsImNyZWF0ZS1jbGllbnQiLCJtYW5hZ2UtdXNlcnMiLCJxdWVyeS1yZWFsbXMiLCJ2aWV3LWF1dGhvcml6YXRpb24iLCJxdWVyeS1jbGllbnRzIiwicXVlcnktdXNlcnMiLCJtYW5hZ2UtZXZlbnRzIiwibWFuYWdlLXJlYWxtIiwidmlldy1ldmVudHMiLCJ2aWV3LXVzZXJzIiwidmlldy1jbGllbnRzIiwibWFuYWdlLWF1dGhvcml6YXRpb24iLCJtYW5hZ2UtY2xpZW50cyIsInF1ZXJ5LWdyb3VwcyJdfSwiQ2xvdWR0cnVzdC1yZWFsbSI6eyJyb2xlcyI6WyJ2aWV3LWlkZW50aXR5LXByb3ZpZGVycyIsInZpZXctcmVhbG0iLCJtYW5hZ2UtaWRlbnRpdHktcHJvdmlkZXJzIiwiaW1wZXJzb25hdGlvbiIsImNyZWF0ZS1jbGllbnQiLCJtYW5hZ2UtdXNlcnMiLCJxdWVyeS1yZWFsbXMiLCJ2aWV3LWF1dGhvcml6YXRpb24iLCJxdWVyeS1jbGllbnRzIiwicXVlcnktdXNlcnMiLCJtYW5hZ2UtZXZlbnRzIiwibWFuYWdlLXJlYWxtIiwidmlldy1ldmVudHMiLCJ2aWV3LXVzZXJzIiwidmlldy1jbGllbnRzIiwibWFuYWdlLWF1dGhvcml6YXRpb24iLCJtYW5hZ2UtY2xpZW50cyIsInF1ZXJ5LWdyb3VwcyJdfSwibWFzdGVyLXJlYWxtIjp7InJvbGVzIjpbInZpZXctcmVhbG0iLCJ2aWV3LWlkZW50aXR5LXByb3ZpZGVycyIsIm1hbmFnZS1pZGVudGl0eS1wcm92aWRlcnMiLCJpbXBlcnNvbmF0aW9uIiwiY3JlYXRlLWNsaWVudCIsIm1hbmFnZS11c2VycyIsInF1ZXJ5LXJlYWxtcyIsInZpZXctYXV0aG9yaXphdGlvbiIsInF1ZXJ5LWNsaWVudHMiLCJxdWVyeS11c2VycyIsIm1hbmFnZS1ldmVudHMiLCJtYW5hZ2UtcmVhbG0iLCJ2aWV3LWV2ZW50cyIsInZpZXctdXNlcnMiLCJ2aWV3LWNsaWVudHMiLCJtYW5hZ2UtYXV0aG9yaXphdGlvbiIsIm1hbmFnZS1jbGllbnRzIiwicXVlcnktZ3JvdXBzIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUgZ3JvdXBzIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJncm91cHMiOlsiL3RvZV9hZG1pbmlzdHJhdG9yIl0sInByZWZlcnJlZF91c2VybmFtZSI6ImFkbWluIn0.GrPUDdQUID0S38ZSwVqarAuSDXrl5cJju3uq32y6bNGPK9a8jcHBP5FEfMcZ3vieQPtWFeySMycwTEcH6x6lc-9bj1w5veL4yyTA1zk_ERPshfiobk0u94vuljnoz-PW7JvBLOy47Bk5cP9pHPbJMPY0kOFHTpXZHd6KwfcE_X8gizLw4rDhIpK1NEtABQVzUNvP9fDZOm2I1PHJbl0odRE7EFu9Xh5ya8DaUQ2RKUb0E5csnA3DYlFdEhtMV1MAKRzqplDzj8zLQ8f8fflzC9_g4vmnDUEKSBxq1f1qKmzm1-XUuqRYTNWHfOtRR9rXrEzn-6fymFcRHIVGW7kgzg`, |
|||
ExpiresIn: 3600, |
|||
} |
|||
var validJSON, _ = json.Marshal(oidcToken) |
|||
|
|||
r := mux.NewRouter() |
|||
r.Handle("/auth/realms/nobody/protocol/openid-connect/token", &TestResponse{StatusCode: http.StatusOK, NoBody: true}) |
|||
r.Handle("/auth/realms/invalid/protocol/openid-connect/token", &TestResponse{StatusCode: http.StatusUnauthorized}) |
|||
r.Handle("/auth/realms/bad-json/protocol/openid-connect/token", &TestResponse{StatusCode: http.StatusOK, ResponseBody: `{"truncated-`}) |
|||
r.Handle("/auth/realms/valid/protocol/openid-connect/token", &TestResponse{StatusCode: http.StatusOK, ResponseBody: string(validJSON)}) |
|||
|
|||
ts := httptest.NewServer(r) |
|||
defer ts.Close() |
|||
|
|||
t.Run("No body in HTTP response", func(t *testing.T) { |
|||
var p = NewOidcTokenProvider(keycloak.Config{AddrTokenProvider: ts.URL}, "nobody", "user", "passwd", "clientID", mockLogger) |
|||
var _, err = p.ProvideToken(context.TODO()) |
|||
assert.NotNil(t, err) |
|||
}) |
|||
|
|||
t.Run("Invalid credentials", func(t *testing.T) { |
|||
var p = NewOidcTokenProvider(keycloak.Config{AddrTokenProvider: ts.URL}, "invalid", "user", "passwd", "clientID", mockLogger) |
|||
var _, err = p.ProvideToken(context.TODO()) |
|||
assert.NotNil(t, err) |
|||
}) |
|||
|
|||
t.Run("Invalid JSON", func(t *testing.T) { |
|||
var p = NewOidcTokenProvider(keycloak.Config{AddrTokenProvider: ts.URL}, "bad-json", "user", "passwd", "clientID", mockLogger) |
|||
var _, err = p.ProvideToken(context.TODO()) |
|||
assert.NotNil(t, err) |
|||
}) |
|||
|
|||
t.Run("No HTTP response", func(t *testing.T) { |
|||
var p = NewOidcTokenProvider(keycloak.Config{AddrTokenProvider: ts.URL + "0"}, "bad-json", "user", "passwd", "clientID", mockLogger) |
|||
var _, err = p.ProvideToken(context.TODO()) |
|||
assert.NotNil(t, err) |
|||
}) |
|||
|
|||
t.Run("Valid credentials", func(t *testing.T) { |
|||
var p = NewOidcTokenProvider(keycloak.Config{AddrTokenProvider: ts.URL}, "valid", "user", "passwd", "clientID", mockLogger) |
|||
|
|||
var timeStart = time.Now() |
|||
|
|||
// First call
|
|||
var token, err = p.ProvideToken(context.TODO()) |
|||
assert.Nil(t, err) |
|||
assert.NotEqual(t, "", token) |
|||
|
|||
var timeAfterFirstCall = time.Now() |
|||
|
|||
// Second call
|
|||
token, err = p.ProvideToken(context.TODO()) |
|||
assert.Nil(t, err) |
|||
assert.NotEqual(t, "", token) |
|||
|
|||
var timeAfterSecondCall = time.Now() |
|||
|
|||
var withHTTPDuration = int64(20 * time.Millisecond) |
|||
var withoutHTTPDuration = int64(5 * time.Millisecond) |
|||
var duration1 = timeAfterFirstCall.Sub(timeStart).Nanoseconds() |
|||
var duration2 = timeAfterSecondCall.Sub(timeAfterFirstCall).Nanoseconds() |
|||
var msg = fmt.Sprintf("Durations: no valid token loaded yet:%d (expected > %d), token not expired:%d (expected < %d)", duration1, withHTTPDuration, duration2, withoutHTTPDuration) |
|||
assert.True(t, duration1 > withHTTPDuration, msg) |
|||
assert.True(t, duration2 < withoutHTTPDuration, msg) |
|||
}) |
|||
} |
|||
@ -1,94 +0,0 @@ |
|||
package toolbox |
|||
|
|||
import ( |
|||
"context" |
|||
"fmt" |
|||
"net/url" |
|||
"sync" |
|||
"time" |
|||
|
|||
"github.com/cloudtrust/keycloak-client/v3" |
|||
oidc "github.com/coreos/go-oidc" |
|||
"github.com/pkg/errors" |
|||
) |
|||
|
|||
// OidcVerifierProvider is an interface for a provider of OidcVerifier instances
|
|||
type OidcVerifierProvider interface { |
|||
GetOidcVerifier(realm string) (OidcVerifier, error) |
|||
} |
|||
|
|||
// OidcVerifier is an interface for OIDC token verifiers
|
|||
type OidcVerifier interface { |
|||
Verify(accessToken string) error |
|||
} |
|||
|
|||
type verifierCache struct { |
|||
duration time.Duration |
|||
errorTolerance time.Duration |
|||
tokenURL *url.URL |
|||
verifiers map[string]cachedVerifier |
|||
verifiersMutex sync.RWMutex |
|||
} |
|||
|
|||
type cachedVerifier struct { |
|||
verifier *oidc.IDTokenVerifier |
|||
createdAt time.Time |
|||
expireAt time.Time |
|||
invalidateOnErrorAt time.Time |
|||
} |
|||
|
|||
// NewVerifierCache create an instance of OIDC verifier cache
|
|||
func NewVerifierCache(tokenURL *url.URL, timeToLive time.Duration, errorTolerance time.Duration) OidcVerifierProvider { |
|||
return &verifierCache{ |
|||
duration: timeToLive, |
|||
errorTolerance: errorTolerance, |
|||
tokenURL: tokenURL, |
|||
verifiers: make(map[string]cachedVerifier), |
|||
verifiersMutex: sync.RWMutex{}, |
|||
} |
|||
} |
|||
|
|||
func (vc *verifierCache) GetOidcVerifier(realm string) (OidcVerifier, error) { |
|||
vc.verifiersMutex.RLock() |
|||
v, ok := vc.verifiers[realm] |
|||
vc.verifiersMutex.RUnlock() |
|||
if ok && v.isValid() { |
|||
return &v, nil |
|||
} |
|||
var oidcProvider *oidc.Provider |
|||
{ |
|||
var err error |
|||
var issuer = fmt.Sprintf("%s/auth/realms/%s", vc.tokenURL.String(), realm) |
|||
oidcProvider, err = oidc.NewProvider(context.Background(), issuer) |
|||
if err != nil { |
|||
return nil, errors.Wrap(err, keycloak.MsgErrCannotCreate+"."+keycloak.OIDCProvider) |
|||
} |
|||
} |
|||
|
|||
ov := oidcProvider.Verifier(&oidc.Config{SkipClientIDCheck: true}) |
|||
res := cachedVerifier{ |
|||
createdAt: time.Now(), |
|||
expireAt: time.Now().Add(vc.duration), |
|||
invalidateOnErrorAt: time.Now().Add(vc.errorTolerance), |
|||
verifier: ov, |
|||
} |
|||
vc.verifiersMutex.Lock() |
|||
vc.verifiers[realm] = res |
|||
vc.verifiersMutex.Unlock() |
|||
|
|||
return &res, nil |
|||
} |
|||
|
|||
func (cv *cachedVerifier) isValid() bool { |
|||
return time.Now().Before(cv.expireAt) |
|||
} |
|||
|
|||
func (cv *cachedVerifier) Verify(accessToken string) error { |
|||
_, err := cv.verifier.Verify(context.Background(), accessToken) |
|||
if err != nil && time.Now().After(cv.invalidateOnErrorAt) { |
|||
// An error occured and current time is after invalidateOnErrorAt
|
|||
// Let's make this verifier expire
|
|||
cv.expireAt = cv.createdAt |
|||
} |
|||
return err |
|||
} |
|||
@ -1,112 +0,0 @@ |
|||
package toolbox |
|||
|
|||
import ( |
|||
"context" |
|||
"encoding/json" |
|||
"net/http" |
|||
"net/http/httptest" |
|||
"net/url" |
|||
"strings" |
|||
"testing" |
|||
"time" |
|||
|
|||
http_transport "github.com/go-kit/kit/transport/http" |
|||
"github.com/gorilla/mux" |
|||
"github.com/stretchr/testify/assert" |
|||
) |
|||
|
|||
func decodeRequest(_ context.Context, req *http.Request) (interface{}, error) { |
|||
res := map[string]string{"realm": mux.Vars(req)["realm"], "host": req.Host} |
|||
return res, nil |
|||
} |
|||
|
|||
func encodeReply(_ context.Context, w http.ResponseWriter, rep interface{}) error { |
|||
if rep == nil { |
|||
w.WriteHeader(404) |
|||
return nil |
|||
} |
|||
w.Header().Set("Content-Type", "application/json; charset=utf-8") |
|||
w.WriteHeader(200) |
|||
|
|||
var json, err = json.Marshal(rep) |
|||
if err == nil { |
|||
w.Write(json) |
|||
} |
|||
return nil |
|||
} |
|||
|
|||
func errorHandler(_ context.Context, _ error, w http.ResponseWriter) { |
|||
w.WriteHeader(500) |
|||
} |
|||
|
|||
func endpoint(_ context.Context, request interface{}) (response interface{}, err error) { |
|||
var query = request.(map[string]string) |
|||
if !strings.Contains(query["realm"], "realm") { |
|||
return nil, nil |
|||
} |
|||
return map[string]string{ |
|||
"issuer": "http://" + query["host"] + "/auth/realms/" + query["realm"], |
|||
"authorization_endpoint": "", |
|||
"token_endpoint": "", |
|||
"jwks_uri": "", |
|||
"userinfo_endpoint": "", |
|||
}, nil |
|||
} |
|||
|
|||
func TestGetOidcVerifier(t *testing.T) { |
|||
verifierHandler := http_transport.NewServer(endpoint, decodeRequest, encodeReply, http_transport.ServerErrorEncoder(errorHandler)) |
|||
|
|||
r := mux.NewRouter() |
|||
r.Handle("/auth/realms/{realm}/.well-known/openid-configuration", verifierHandler) |
|||
|
|||
ts := httptest.NewServer(r) |
|||
defer ts.Close() |
|||
|
|||
url, _ := url.Parse(ts.URL) |
|||
|
|||
{ |
|||
// First test with a verifier which hardly expires
|
|||
verifier := NewVerifierCache(url, time.Minute, 10*time.Minute) |
|||
|
|||
{ |
|||
// Unknown realm: can't get verifier
|
|||
_, err := verifier.GetOidcVerifier("unknown") |
|||
assert.NotNil(t, err) |
|||
} |
|||
|
|||
v1, e := verifier.GetOidcVerifier("realm1") |
|||
assert.Nil(t, e) |
|||
{ |
|||
// Ask for the same realm before its verifier expires
|
|||
v2, _ := verifier.GetOidcVerifier("realm1") |
|||
assert.Equal(t, v1, v2) |
|||
} |
|||
{ |
|||
// Ask for a different verifier
|
|||
v3, _ := verifier.GetOidcVerifier("realm2") |
|||
assert.NotEqual(t, v1, v3) |
|||
} |
|||
|
|||
time.Sleep(100 * time.Millisecond) |
|||
assert.NotNil(t, v1.Verify("abcdef")) |
|||
} |
|||
|
|||
{ |
|||
// Now, test with a verifier which quickly expires on error
|
|||
verifier := NewVerifierCache(url, time.Minute, time.Millisecond) |
|||
v1, _ := verifier.GetOidcVerifier("realm1") |
|||
time.Sleep(100 * time.Millisecond) |
|||
{ |
|||
// Ask for the same realm before its verifier expires
|
|||
v2, _ := verifier.GetOidcVerifier("realm1") |
|||
assert.Equal(t, v1, v2) |
|||
} |
|||
{ |
|||
// Verify an invalid token
|
|||
assert.NotNil(t, v1.Verify("abcdef")) |
|||
// Ask for the same realm before its verifier expires but after an error occured
|
|||
v2, _ := verifier.GetOidcVerifier("realm1") |
|||
assert.NotEqual(t, v1, v2) |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue