Browse Source

remove unneeded features

master
Nicolas Massé 5 years ago
parent
commit
7e5bb100e6
  1. 301
      Gopkg.lock
  2. 37
      Gopkg.toml
  3. 67
      Jenkinsfile
  4. 107
      api/account.go
  5. 46
      api/credentials.go
  6. 49
      api/keycloak_client.go
  7. BIN
      integration/integration
  8. 35
      integration/integration-tests.go
  9. 66
      toolbox/issuer.go
  10. 51
      toolbox/issuer_test.go
  11. 8
      toolbox/logger.go
  12. 1
      toolbox/mock/keep.go
  13. 3
      toolbox/mock_test.go
  14. 102
      toolbox/oidc_connect.go
  15. 107
      toolbox/oidc_connect_test.go
  16. 94
      toolbox/oidc_verifier.go
  17. 112
      toolbox/oidc_verifier_test.go

301
Gopkg.lock

@ -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

37
Gopkg.toml

@ -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"

67
Jenkinsfile

@ -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}
"""
}
}
}
}
}
}

107
api/account.go

@ -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
}

46
api/credentials.go

@ -1,10 +1,9 @@
package api
import (
"strconv"
"github.com/cloudtrust/keycloak-client/v3"
"gopkg.in/h2non/gentleman.v2/plugins/body"
"gopkg.in/h2non/gentleman.v2/plugins/headers"
"gopkg.in/h2non/gentleman.v2/plugins/url"
)
@ -16,10 +15,11 @@ const (
labelPath = credentialIDPath + "/label"
moveFirstPath = credentialIDPath + "/moveToFirst"
moveAfterPath = credentialIDPath + "/moveAfter/:previousCredentialID"
// Paper card API
papercardPath = "/auth/realms/:realm/papercard"
resetFailuresPath = papercardPath + "/users/:userId/credentials/:credentialId/resetFailures"
sendPaperCardsRemindersPath = papercardPath + "/expiryReminders"
)
var (
hdrAcceptJSON = headers.Set("Accept", "application/json")
hdrContentTypeTextPlain = headers.Set("Content-Type", "text/plain")
)
// ResetPassword resets password of the user.
@ -63,37 +63,3 @@ func (c *Client) MoveAfter(accessToken string, realmName string, userID string,
_, err := c.post(accessToken, url.Path(moveAfterPath), url.Param("realm", realmName), url.Param("id", userID), url.Param("credentialID", credentialID), url.Param("previousCredentialID", previousCredentialID))
return err
}
// UpdatePassword updates the user's password
// Parameters: realm, currentPassword, newPassword, confirmPassword
func (c *Client) UpdatePassword(accessToken, realm, currentPassword, newPassword, confirmPassword string) (string, error) {
var m = map[string]string{"currentPassword": currentPassword, "newPassword": newPassword, "confirmation": confirmPassword}
return c.post(accessToken, nil, url.Path(accountPasswordPath), url.Param("realm", realm), body.JSON(m))
}
// ResetPapercardFailures reset failures information in a paper card credential
func (c *Client) ResetPapercardFailures(accessToken, realmName, userID, credentialID string) error {
return c.put(accessToken, url.Path(resetFailuresPath), url.Param("realm", realmName), url.Param("userId", userID), url.Param("credentialId", credentialID))
}
// RemindersResponse struct
type RemindersResponse struct {
Partial bool `json:"partial"`
}
// SendPaperCardsReminders sends reminders to users of paper cards which will soon be expired
func (c *Client) SendPaperCardsReminders(accessToken, realmName string, firstReminderDays, nextReminderDays, maxCount int) (bool, error) {
var paramKV = []string{
"firstReminderDays", strconv.Itoa(firstReminderDays),
"nextReminderDays", strconv.Itoa(nextReminderDays),
"maxCount", strconv.Itoa(maxCount),
}
var resp RemindersResponse
var plugins = append(createQueryPlugins(paramKV...), url.Path(sendPaperCardsRemindersPath), url.Param("realm", realmName))
var _, err = c.post(accessToken, &resp, plugins...)
if err != nil {
return false, err
}
return resp.Partial, err
}

49
api/keycloak_client.go

@ -10,7 +10,6 @@ import (
commonhttp "github.com/cloudtrust/common-service/errors"
"github.com/cloudtrust/keycloak-client/v3"
"github.com/cloudtrust/keycloak-client/v3/toolbox"
"github.com/pkg/errors"
"gopkg.in/h2non/gentleman.v2"
"gopkg.in/h2non/gentleman.v2/plugin"
@ -22,28 +21,12 @@ import (
// Client is the keycloak client.
type Client struct {
apiURL *url.URL
httpClient *gentleman.Client
account *AccountClient
issuerManager toolbox.IssuerManager
}
// AccountClient structure
type AccountClient struct {
client *Client
apiURL *url.URL
httpClient *gentleman.Client
}
// New returns a keycloak client.
func New(config keycloak.Config) (*Client, error) {
var issuerMgr toolbox.IssuerManager
{
var err error
issuerMgr, err = toolbox.NewIssuerManager(config)
if err != nil {
return nil, errors.Wrap(err, keycloak.MsgErrCannotParse+"."+keycloak.TokenProviderURL)
}
}
var uAPI *url.URL
{
var err error
@ -60,13 +43,8 @@ func New(config keycloak.Config) (*Client, error) {
}
var client = &Client{
apiURL: uAPI,
httpClient: httpClient,
issuerManager: issuerMgr,
}
client.account = &AccountClient{
client: client,
apiURL: uAPI,
httpClient: httpClient,
}
return client, nil
@ -118,25 +96,6 @@ func (c *Client) GetToken(realm string, username string, password string) (strin
return accessToken.(string), nil
}
// VerifyToken verifies a token. It returns an error it is malformed, expired,...
func (c *Client) VerifyToken(issuer string, realmName string, accessToken string) error {
oidcVerifierProvider, err := c.issuerManager.GetOidcVerifierProvider(issuer)
if err != nil {
return err
}
verifier, err := oidcVerifierProvider.GetOidcVerifier(realmName)
if err != nil {
return err
}
return verifier.Verify(accessToken)
}
// AccountClient gets the associated AccountClient
func (c *Client) AccountClient() *AccountClient {
return c.account
}
// get is a HTTP get method.
func (c *Client) get(accessToken string, data interface{}, plugins ...plugin.Plugin) error {
var err error

BIN
integration/integration

Binary file not shown.

35
integration/integration_test.go → integration/integration-tests.go

@ -11,16 +11,13 @@ import (
"github.com/spf13/pflag"
)
type keyContext int
const (
tstRealm = "__internal"
reqRealm = "master"
user = "version"
)
func main() {
var conf = getKeycloakConfig()
fmt.Printf("Connecting to KC %s...\n", conf.AddrAPI)
var client, err = api.New(*conf)
if err != nil {
log.Fatalf("could not create keycloak client: %v", err)
@ -32,11 +29,6 @@ func main() {
log.Fatalf("could not get access token: %v", err)
}
err = client.VerifyToken("issuer", "master", accessToken)
if err != nil {
log.Fatalf("could not validate access token: %v", err)
}
// Delete test realm
client.DeleteRealm(accessToken, tstRealm)
@ -171,7 +163,7 @@ func main() {
}
{
// email.
var users, err = client.GetUsers(accessToken, reqRealm, tstRealm, "email", "john.doe@cloudtrust.ch")
var users, err = client.GetUsers(accessToken, tstRealm, "email", "john.doe@cloudtrust.ch")
if err != nil {
log.Fatalf("could not get users: %v", err)
}
@ -181,7 +173,7 @@ func main() {
}
{
// firstname.
var users, err = client.GetUsers(accessToken, reqRealm, tstRealm, "firstName", "John")
var users, err = client.GetUsers(accessToken, tstRealm, "firstName", "John")
if err != nil {
log.Fatalf("could not get users: %v", err)
}
@ -192,7 +184,7 @@ func main() {
}
{
// lastname.
var users, err = client.GetUsers(accessToken, reqRealm, tstRealm, "lastName", "Wells")
var users, err = client.GetUsers(accessToken, tstRealm, "lastName", "Wells")
if err != nil {
log.Fatalf("could not get users: %v", err)
}
@ -202,7 +194,7 @@ func main() {
}
{
// username.
var users, err = client.GetUsers(accessToken, reqRealm, tstRealm, "username", "lucia.nelson")
var users, err = client.GetUsers(accessToken, tstRealm, "username", "lucia.nelson")
if err != nil {
log.Fatalf("could not get users: %v", err)
}
@ -212,7 +204,7 @@ func main() {
}
{
// first.
var users, err = client.GetUsers(accessToken, reqRealm, tstRealm, "max", "7")
var users, err = client.GetUsers(accessToken, tstRealm, "max", "7")
if err != nil {
log.Fatalf("could not get users: %v", err)
}
@ -222,7 +214,7 @@ func main() {
}
{
// search.
var users, err = client.GetUsers(accessToken, reqRealm, tstRealm, "search", "le")
var users, err = client.GetUsers(accessToken, tstRealm, "search", "le")
if err != nil {
log.Fatalf("could not get users: %v", err)
}
@ -239,7 +231,7 @@ func main() {
// Get user ID.
var userID string
{
var users, err = client.GetUsers(accessToken, reqRealm, tstRealm, "search", "Maria")
var users, err = client.GetUsers(accessToken, tstRealm, "search", "Maria")
if err != nil {
log.Fatalf("could not get Maria: %v", err)
}
@ -266,7 +258,7 @@ func main() {
}
// Check that user was updated.
{
var users, err = client.GetUsers(accessToken, reqRealm, tstRealm, "search", "Maria")
var users, err = client.GetUsers(accessToken, tstRealm, "search", "Maria")
if err != nil {
log.Fatalf("could not get Maria: %v", err)
}
@ -280,8 +272,7 @@ func main() {
fmt.Println("User updated.")
// Check credentials
{
tstRealmReq := "master"
var creds, err = client.GetCredentials(accessToken, tstRealmReq, userID)
var creds, err = client.GetCredentials(accessToken, tstRealm, userID)
if err != nil {
log.Fatalf("could not get credentials: %v", err)
}
@ -296,7 +287,7 @@ func main() {
// Get user ID.
var userID string
{
var users, err = client.GetUsers(accessToken, reqRealm, tstRealm, "search", "Toni")
var users, err = client.GetUsers(accessToken, tstRealm, "search", "Toni")
if err != nil {
log.Fatalf("could not get Toni: %v", err)
}
@ -351,8 +342,8 @@ func main() {
}
func getKeycloakConfig() *keycloak.Config {
var apiAddr = pflag.String("urlKc", "http://localhost:8080", "keycloak address")
var tokenAddr = pflag.String("url", "http://localhost:8080", "token address")
var apiAddr = pflag.String("urlKc", "http://localhost:8080/auth", "keycloak address")
var tokenAddr = pflag.String("url", "http://localhost:8080/auth/realms/master", "token address")
pflag.Parse()
return &keycloak.Config{

66
toolbox/issuer.go

@ -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")
}

51
toolbox/issuer_test.go

@ -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)
}

8
toolbox/logger.go

@ -1,8 +0,0 @@
package toolbox
import "context"
// Logger interface for logging with level
type Logger interface {
Warn(ctx context.Context, keyvals ...interface{})
}

1
toolbox/mock/keep.go

@ -1 +0,0 @@
package mock

3
toolbox/mock_test.go

@ -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

102
toolbox/oidc_connect.go

@ -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
}

107
toolbox/oidc_connect_test.go

@ -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)
})
}

94
toolbox/oidc_verifier.go

@ -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
}

112
toolbox/oidc_verifier_test.go

@ -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…
Cancel
Save