diff --git a/Gopkg.lock b/Gopkg.lock deleted file mode 100644 index ecbc6db..0000000 --- a/Gopkg.lock +++ /dev/null @@ -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 diff --git a/Gopkg.toml b/Gopkg.toml deleted file mode 100644 index 31e3f10..0000000 --- a/Gopkg.toml +++ /dev/null @@ -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" diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 6629c46..0000000 --- a/Jenkinsfile +++ /dev/null @@ -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} - """ - } - } - } - } - } -} diff --git a/api/account.go b/api/account.go deleted file mode 100644 index 14fc04f..0000000 --- a/api/account.go +++ /dev/null @@ -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 -} diff --git a/api/credentials.go b/api/credentials.go index c948269..ba02e4d 100644 --- a/api/credentials.go +++ b/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 -} diff --git a/api/keycloak_client.go b/api/keycloak_client.go index 7c15998..64193c7 100644 --- a/api/keycloak_client.go +++ b/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 diff --git a/integration/integration b/integration/integration deleted file mode 100755 index f473963..0000000 Binary files a/integration/integration and /dev/null differ diff --git a/integration/integration_test.go b/integration/integration-tests.go similarity index 87% rename from integration/integration_test.go rename to integration/integration-tests.go index 19b7d81..aa3f09f 100644 --- a/integration/integration_test.go +++ b/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{ diff --git a/toolbox/issuer.go b/toolbox/issuer.go deleted file mode 100644 index 375977d..0000000 --- a/toolbox/issuer.go +++ /dev/null @@ -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") -} diff --git a/toolbox/issuer_test.go b/toolbox/issuer_test.go deleted file mode 100644 index b6389a1..0000000 --- a/toolbox/issuer_test.go +++ /dev/null @@ -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) -} diff --git a/toolbox/logger.go b/toolbox/logger.go deleted file mode 100644 index 470e777..0000000 --- a/toolbox/logger.go +++ /dev/null @@ -1,8 +0,0 @@ -package toolbox - -import "context" - -// Logger interface for logging with level -type Logger interface { - Warn(ctx context.Context, keyvals ...interface{}) -} diff --git a/toolbox/mock/keep.go b/toolbox/mock/keep.go deleted file mode 100644 index f33e02b..0000000 --- a/toolbox/mock/keep.go +++ /dev/null @@ -1 +0,0 @@ -package mock diff --git a/toolbox/mock_test.go b/toolbox/mock_test.go deleted file mode 100644 index 0b7aa8d..0000000 --- a/toolbox/mock_test.go +++ /dev/null @@ -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 diff --git a/toolbox/oidc_connect.go b/toolbox/oidc_connect.go deleted file mode 100644 index 8c47316..0000000 --- a/toolbox/oidc_connect.go +++ /dev/null @@ -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 -} diff --git a/toolbox/oidc_connect_test.go b/toolbox/oidc_connect_test.go deleted file mode 100644 index b4542cd..0000000 --- a/toolbox/oidc_connect_test.go +++ /dev/null @@ -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) - }) -} diff --git a/toolbox/oidc_verifier.go b/toolbox/oidc_verifier.go deleted file mode 100644 index c9ead2c..0000000 --- a/toolbox/oidc_verifier.go +++ /dev/null @@ -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 -} diff --git a/toolbox/oidc_verifier_test.go b/toolbox/oidc_verifier_test.go deleted file mode 100644 index a76f271..0000000 --- a/toolbox/oidc_verifier_test.go +++ /dev/null @@ -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) - } - } -}