├── ci ├── install │ ├── inits │ │ ├── systemd │ │ │ ├── default │ │ │ │ └── tyk-identity-broker │ │ │ └── system │ │ │ │ └── tyk-identity-broker.service │ │ ├── upstart │ │ │ ├── default │ │ │ │ └── tyk-identity-broker │ │ │ └── init │ │ │ │ ├── 1.x │ │ │ │ └── tyk-identity-broker.conf │ │ │ │ └── 0.x │ │ │ │ └── tyk-identity-broker.conf │ │ └── sysv │ │ │ ├── default │ │ │ └── tyk-identity-broker │ │ │ └── init.d │ │ │ └── tyk-identity-broker │ ├── before_install.sh │ ├── post_trans.sh │ ├── post_remove.sh │ └── post_install.sh ├── goreleaser │ ├── goreleaser-el7.yml │ ├── goreleaser.yml │ └── .goreleaser.yml ├── Dockerfile.distroless ├── terraform │ ├── outputs.tf │ └── terraform │ │ └── outputs.tf ├── bin │ ├── integration_build.sh │ ├── dist_push.sh │ ├── unlock-agent.sh │ ├── pc.sh │ └── dist_build.sh └── Dockerfile.std ├── tap ├── tapProvider │ └── tapProvider.go ├── identity-handlers │ ├── uuid.go │ ├── dummy_handler.go │ └── tyk_handler_test.go ├── provider_type.go ├── authregister_backend.go ├── ta_provider.go ├── identity_handler.go ├── action.go ├── profileActions.go └── profile.go ├── .DS_Store ├── .github ├── CODEOWNERS ├── workflows │ ├── jira-pr-validator.yaml │ ├── visor.yaml │ ├── release-tests.yml │ └── ci-tests.yml ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── stale.yml └── PULL_REQUEST_TEMPLATE.md ├── version.go ├── .gitignore ├── constants └── constants.go ├── backends ├── mongo_test.go ├── in_memory_test.go ├── in_memory.go ├── mongo.go ├── redis.go └── redis_test.go ├── providers ├── util_rand_string.go ├── util_ba_extractor.go ├── saml_test.go ├── util_slug.go ├── tapProvider.go ├── FileLoader.go ├── proxy_test.go ├── proxy.go ├── social.go └── reverse_proxy.go ├── data_loader ├── dumb_loader.go ├── mongo_loader_test.go ├── data_loader_test.go ├── data_loader.go ├── file_loader.go └── mongo_loader.go ├── bin ├── merge-cov.sh └── ci-tests.sh ├── initializer ├── initializer_test.go └── initializer.go ├── configuration ├── testdata │ └── tib_test.conf ├── config.go └── config_test.go ├── internal └── jwe │ ├── testing.go │ ├── JWE.go │ ├── testing_test.go │ └── JWE_test.go ├── tib_sample.conf ├── docker ├── Dockerfile └── hooks │ └── build ├── error └── error.go ├── log └── log.go ├── toth └── toth.go ├── http_handlers.go ├── tothic ├── tothic_test.go └── tothic.go ├── api.go ├── main.go └── go.mod /ci/install/inits/systemd/default/tyk-identity-broker: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ci/install/inits/upstart/default/tyk-identity-broker: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tap/tapProvider/tapProvider.go: -------------------------------------------------------------------------------- 1 | package tapProvider 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TykTechnologies/tyk-identity-broker/HEAD/.DS_Store -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /ci/ @TykTechnologies/devops 2 | .github/workflows/release.yml @TykTechnologies/devops 3 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var Version = "v1.7.0" 4 | var Commit, BuildDate, BuildTime, BuiltBy string 5 | -------------------------------------------------------------------------------- /ci/install/inits/sysv/default/tyk-identity-broker: -------------------------------------------------------------------------------- 1 | user="tyk" 2 | group="tyk" 3 | chroot="/" 4 | chdir="/opt/tyk-identity-broker" 5 | nice="" 6 | 7 | -------------------------------------------------------------------------------- /tap/identity-handlers/uuid.go: -------------------------------------------------------------------------------- 1 | package identityHandlers 2 | 3 | import ( 4 | "github.com/gofrs/uuid" 5 | ) 6 | 7 | func newUUID() string { 8 | id, err := uuid.NewV4() 9 | if err != nil { 10 | panic(err) 11 | } 12 | return id.String() 13 | } 14 | -------------------------------------------------------------------------------- /ci/goreleaser/goreleaser-el7.yml: -------------------------------------------------------------------------------- 1 | # Generated by: gromit policy 2 | # Generated on: Fri Apr 28 10:48:32 UTC 2023 3 | 4 | # Check the documentation at http://goreleaser.com 5 | # This project needs CGO_ENABLED=1 and the cross-compiler toolchains for 6 | # - arm64 7 | # - amd64 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ._* 2 | tib.conf 3 | *.json 4 | certs/ 5 | tyk-auth-proxy 6 | tyk-auth-proxy_linux_* 7 | release/ 8 | build/ 9 | build_tools/ 10 | ./tyk-identity-broker 11 | tyk-identity-broker 12 | tyk.io.signing.key 13 | # IDEA 14 | .idea 15 | *.iml 16 | *.swp 17 | *~ 18 | .terraform** 19 | .DS_Store 20 | -------------------------------------------------------------------------------- /ci/install/before_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Generated by: gromit policy 4 | 5 | echo "Creating user and group..." 6 | GROUPNAME="tyk" 7 | USERNAME="tyk" 8 | 9 | getent group "$GROUPNAME" >/dev/null || groupadd -r "$GROUPNAME" 10 | getent passwd "$USERNAME" >/dev/null || useradd -r -g "$GROUPNAME" -M -s /sbin/nologin -c "Tyk service user" "$USERNAME" 11 | -------------------------------------------------------------------------------- /constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // HandlerLogTag is a tag we are using to identify log messages from the handler 5 | HandlerLogTag = "AUTH HANDLERS" 6 | ) 7 | 8 | // providers 9 | const ( 10 | SocialProvider = "SocialProvider" 11 | ADProvider = "ADProvider" 12 | ProxyProvider = "ProxyProvider" 13 | SAMLProvider = "SAMLProvider" 14 | ) 15 | -------------------------------------------------------------------------------- /backends/mongo_test.go: -------------------------------------------------------------------------------- 1 | package backends 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | // TestInit tests the Init method of the MongoBackend 10 | func TestMongoInit(t *testing.T) { 11 | // Create an instance of MongoBackend 12 | m := MongoBackend{} 13 | 14 | // Call the Init method 15 | err := m.Init(nil) 16 | 17 | // Assert that Init returns nil 18 | assert.Nil(t, err) 19 | } 20 | -------------------------------------------------------------------------------- /tap/provider_type.go: -------------------------------------------------------------------------------- 1 | /* 2 | package tap wraps a set of interfaces and object to provide a generic interface to a delegated authentication 3 | 4 | proxy 5 | */ 6 | package tap 7 | 8 | // ProviderType is a way of identitying whether a provider passes through or redirects 9 | type ProviderType string 10 | 11 | const ( 12 | PASSTHROUGH_PROVIDER ProviderType = "passthrough" 13 | REDIRECT_PROVIDER ProviderType = "redirect" 14 | ) 15 | -------------------------------------------------------------------------------- /providers/util_rand_string.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | func init() { 9 | rand.Seed(time.Now().UnixNano()) 10 | } 11 | 12 | var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 13 | 14 | func RandStringRunes(n int) string { 15 | b := make([]rune, n) 16 | for i := range b { 17 | b[i] = letterRunes[rand.Intn(len(letterRunes))] 18 | } 19 | return string(b) 20 | } 21 | -------------------------------------------------------------------------------- /ci/install/post_trans.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Generated by: gromit policy 4 | 5 | if command -V systemctl >/dev/null 2>&1; then 6 | if [ ! -f /lib/systemd/system/tyk-identity-broker.service ]; then 7 | cp /opt/tyk-identity-broker/install/inits/systemd/system/tyk-identity-broker.service /lib/systemd/system/tyk-identity-broker.service 8 | fi 9 | else 10 | if [ ! -f /etc/init.d/tyk-identity-broker ]; then 11 | cp /opt/tyk-identity-broker/install/inits/sysv/init.d/tyk-identity-broker /etc/init.d/tyk-identity-broker 12 | fi 13 | fi 14 | -------------------------------------------------------------------------------- /data_loader/dumb_loader.go: -------------------------------------------------------------------------------- 1 | package data_loader 2 | 3 | import "github.com/TykTechnologies/tyk-identity-broker/tap" 4 | 5 | // DumbLoader does nothing, use for those cases where cache not needed so it's the same data store 6 | // so call Flush and LoadIntoStore doesnt make sense 7 | type DumbLoader struct{} 8 | 9 | func (DumbLoader) Init(conf interface{}) error { 10 | return nil 11 | } 12 | 13 | func (DumbLoader) LoadIntoStore(tap.AuthRegisterBackend) error { 14 | return nil 15 | } 16 | 17 | func (DumbLoader) Flush(tap.AuthRegisterBackend) error { 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /ci/Dockerfile.distroless: -------------------------------------------------------------------------------- 1 | # Generated by: gromit policy 2 | 3 | FROM debian:bookworm-slim as DEB 4 | ARG TARGETARCH 5 | 6 | ENV DEBIAN_FRONTEND=noninteractive 7 | 8 | COPY *${TARGETARCH}.deb / 9 | RUN rm -f /*fips*.deb && dpkg -i /tyk-identity-broker*${TARGETARCH}.deb && rm /*.deb 10 | 11 | FROM gcr.io/distroless/static-debian12:nonroot 12 | 13 | COPY --from=DEB /opt/tyk-identity-broker /opt/tyk-identity-broker 14 | 15 | ARG PORTS 16 | EXPOSE $PORTS 17 | 18 | WORKDIR /opt/tyk-identity-broker/ 19 | 20 | ENTRYPOINT ["/opt/tyk-identity-broker/tyk-identity-broker" ] 21 | CMD [ "--conf=/opt/tyk-identity-broker/tib.conf" ] 22 | -------------------------------------------------------------------------------- /.github/workflows/jira-pr-validator.yaml: -------------------------------------------------------------------------------- 1 | name: Validate PR against Jira 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened, edited] 6 | 7 | concurrency: 8 | group: jira-validator-${{ github.event.pull_request.number }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | validate: 13 | if: ${{ !github.event.pull_request.draft }} 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Validate Jira ticket 17 | uses: TykTechnologies/jira-linter@main 18 | with: 19 | jira-base-url: 'https://tyktech.atlassian.net' 20 | jira-api-token: ${{ secrets.JIRA_TOKEN }} 21 | 22 | -------------------------------------------------------------------------------- /ci/terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | 2 | # Generated by: gromit policy 3 | # Generated on: Fri Apr 28 10:48:32 UTC 2023 4 | 5 | 6 | 7 | data "terraform_remote_state" "integration" { 8 | backend = "remote" 9 | 10 | config = { 11 | organization = "Tyk" 12 | workspaces = { 13 | name = "base-prod" 14 | } 15 | } 16 | } 17 | 18 | output "tyk-identity-broker" { 19 | value = data.terraform_remote_state.integration.outputs.tyk-identity-broker 20 | description = "ECR creds for tyk-identity-broker repo" 21 | } 22 | 23 | output "region" { 24 | value = data.terraform_remote_state.integration.outputs.region 25 | description = "Region in which the env is running" 26 | } 27 | -------------------------------------------------------------------------------- /providers/util_ba_extractor.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | b64 "encoding/base64" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | func ExtractBAUsernameAndPasswordFromRequest(r *http.Request) (string, string) { 10 | uName := "" 11 | pw := "" 12 | authHeader := r.Header.Get("Authorization") 13 | splitFields := strings.Split(authHeader, " ") 14 | if len(splitFields) == 2 { 15 | upEnc, decErr := b64.StdEncoding.DecodeString(splitFields[1]) 16 | if decErr == nil { 17 | // split out again 18 | splitUP := strings.Split(string(upEnc), ":") 19 | if len(splitUP) == 2 { 20 | uName = splitUP[0] 21 | pw = splitUP[1] 22 | } 23 | } 24 | } 25 | 26 | return uName, pw 27 | } 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | 2 | # Generated by: gromit policy 3 | # Generated on: Wed Nov 29 23:09:27 UTC 2023 4 | 5 | version: 2 6 | updates: 7 | # Maintain dependencies for GitHub Actions 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | 13 | - package-ecosystem: "gomod" 14 | # Look for `go.mod` file in the `root` directory 15 | directory: "/" 16 | # Check the gomod registry for updates every Monday 17 | schedule: 18 | interval: "weekly" 19 | reviewers: 20 | - "TykTechnologies/platform-squad" 21 | # max number of pull requests that dependabot will open in tandem 22 | open-pull-requests-limit: 4 23 | -------------------------------------------------------------------------------- /bin/merge-cov.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | export GO111MODULE=on 6 | 7 | # print a command and execute it 8 | show() { 9 | echo "$@" >&2 10 | eval "$@" 11 | } 12 | 13 | fatal() { 14 | echo "$@" >&2 15 | exit 1 16 | } 17 | 18 | for pkg in $(go list github.com/TykTechnologies/tyk-identity-broker/...); 19 | do 20 | coveragefile=`echo "$pkg.cov" | awk -F/ '{print $NF}'` 21 | mgo_cov=`echo "$pkg-mongo-mgo.cov" | awk -F/ '{print $NF}'` 22 | mongo_cov=`echo "$pkg-mongo-official.cov" | awk -F/ '{print $NF}'` 23 | file_cov=`echo "$pkg-file.cov" | awk -F/ '{print $NF}'` 24 | show gocovmerge $mongo_cov $mgo_cov $file_cov > $coveragefile 25 | rm $mongo_cov $mgo_cov $file_cov 26 | done -------------------------------------------------------------------------------- /tap/authregister_backend.go: -------------------------------------------------------------------------------- 1 | /* 2 | package tap wraps a set of interfaces and object to provide a generic interface to a delegated authentication 3 | 4 | proxy 5 | */ 6 | package tap 7 | 8 | import ( 9 | "github.com/TykTechnologies/storage/persistent/model" 10 | ) 11 | 12 | // AuthRegisterBackend is an interface to provide storage for profiles loaded into TAP 13 | type AuthRegisterBackend interface { 14 | Init(interface{}) error 15 | SetKey(key string, orgId string, val interface{}) error 16 | GetKey(key string, orgId string, val interface{}) error 17 | GetAll(orgId string) []interface{} 18 | DeleteKey(key string, orgId string) error 19 | } 20 | 21 | type DBObject interface { 22 | SetDBID(id model.ObjectID) 23 | } 24 | -------------------------------------------------------------------------------- /ci/install/inits/systemd/system/tyk-identity-broker.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Tyk Identity Broker 3 | Wants=network-online.target 4 | After=network-online.target 5 | 6 | [Service] 7 | Type=simple 8 | User=tyk 9 | Group=tyk 10 | # Load env vars from /etc/default/ and /etc/sysconfig/ if they exist. 11 | # Prefixing the path with '-' makes it try to load, but if the file doesn't 12 | # exist, it continues onward. 13 | EnvironmentFile=-/etc/default/tyk-identity-broker 14 | EnvironmentFile=-/etc/sysconfig/tyk-identity-broker 15 | ExecStart=/opt/tyk-identity-broker/tyk-identity-broker -c /opt/tyk-identity-broker/tib.conf 16 | Restart=always 17 | WorkingDirectory=/opt/tyk-identity-broker 18 | 19 | [Install] 20 | WantedBy=multi-user.target 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement, front-end 6 | assignees: ConsM, jay-deshmukh, lghiur 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /initializer/initializer_test.go: -------------------------------------------------------------------------------- 1 | package initializer 2 | 3 | import ( 4 | "testing" 5 | 6 | temporal "github.com/TykTechnologies/storage/temporal/keyvalue" 7 | "github.com/TykTechnologies/tyk-identity-broker/backends" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCreateBackendFromRedisConn(t *testing.T) { 12 | var kv temporal.KeyValue 13 | keyPrefix := "test-prefix" 14 | 15 | // Call the function 16 | result := CreateBackendFromRedisConn(kv, keyPrefix) 17 | 18 | // Assert that result is not nil 19 | assert.NotNil(t, result) 20 | redisBackend, ok := result.(*backends.RedisBackend) 21 | assert.True(t, ok) 22 | 23 | // Assert that the KeyPrefix is correctly set 24 | assert.Equal(t, keyPrefix, redisBackend.KeyPrefix) 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/visor.yaml: -------------------------------------------------------------------------------- 1 | name: Visor 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | issues: 7 | types: [opened] 8 | issue_comment: 9 | types: [created] 10 | 11 | permissions: 12 | contents: read 13 | pull-requests: write 14 | issues: write 15 | checks: write 16 | 17 | jobs: 18 | visor: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v4 23 | - uses: probelabs/visor@main 24 | with: 25 | app-id: ${{ secrets.PROBE_APP_ID }} 26 | private-key: ${{ secrets.PROBE_APP_PRIVATE_KEY }} 27 | installation-id: ${{ secrets.PROBE_APP_INSTALLATION_ID }} 28 | env: 29 | GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} 30 | -------------------------------------------------------------------------------- /backends/in_memory_test.go: -------------------------------------------------------------------------------- 1 | package backends 2 | 3 | import "testing" 4 | 5 | func TestInMemoryBackend_GetAndSetKey(t *testing.T) { 6 | backend := &InMemoryBackend{} 7 | 8 | var config interface{} 9 | 10 | backend.Init(config) 11 | 12 | type aStruct struct { 13 | Thing string 14 | } 15 | 16 | saveVal := aStruct{Thing: "Test"} 17 | keyName := "test-key" 18 | 19 | sErr := backend.SetKey(keyName, "", saveVal) 20 | if sErr != nil { 21 | t.Error("Error raised on set key: ", sErr) 22 | } 23 | 24 | target := aStruct{} 25 | vErr := backend.GetKey(keyName, "", &target) 26 | 27 | if vErr != nil { 28 | t.Error("Error raised on get key: ", vErr) 29 | } 30 | 31 | if target.Thing != saveVal.Thing { 32 | t.Error("Expected 'Test' as key val, got: ", target.Thing) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ci/bin/integration_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | : ${ARCH:=amd64} 6 | : ${PKG_PREFIX:=tyk-identity-broker} 7 | 8 | DESCRIPTION="TBD" 9 | BUILD_DIR="build" 10 | 11 | function test { 12 | "$@" 13 | local status=$? 14 | if [ $status -ne 0 ]; then 15 | echo "error with $1" >&2 16 | exit 1 17 | fi 18 | return $status 19 | } 20 | 21 | # ---- APP BUILD START --- 22 | echo "Building application" 23 | test go build 24 | # ---- APP BUILD END --- 25 | 26 | mkdir $BUILD_DIR 27 | # ---- CREATE TARGET FOLDER --- 28 | echo "Copying Dashboard files" 29 | cp -R app $BUILD_DIR/ 30 | cp -R public $BUILD_DIR/ 31 | cp README.md $BUILD_DIR/ 32 | 33 | echo "Creating $arch Tarball" 34 | mv tyk-identity-broker $BUILD_DIR 35 | tar -C $BUILD_DIR -pczf ${PKG_PREFIX}-$ARCH-$VERSION.tar.gz . 36 | -------------------------------------------------------------------------------- /ci/terraform/terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | # Generated by: tyk-ci/wf-gen 2 | # Generated on: Fri Jan 14 17:45:41 UTC 2022 3 | 4 | # Generation commands: 5 | # ./pr.zsh -base v1.2.2-rc5 -branch v1.2.2-rc5-m4-sync -title sync m4 templates -repos tyk-identity-broker 6 | # m4 -E -DxREPO=tyk-identity-broker 7 | 8 | 9 | data "terraform_remote_state" "integration" { 10 | backend = "remote" 11 | 12 | config = { 13 | organization = "Tyk" 14 | workspaces = { 15 | name = "base-prod" 16 | } 17 | } 18 | } 19 | 20 | output "tyk-identity-broker" { 21 | value = data.terraform_remote_state.integration.outputs.tyk-identity-broker 22 | description = "ECR creds for tyk-identity-broker repo" 23 | } 24 | 25 | output "region" { 26 | value = data.terraform_remote_state.integration.outputs.region 27 | description = "Region in which the env is running" 28 | } 29 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 90 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 14 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - Priority:Urgent 10 | - Priority:High 11 | # Label to use when marking an issue as stale 12 | staleLabel: wontfix 13 | # Comment to post when marking an issue as stale. Set to `false` to disable 14 | markComment: > 15 | This issue has been automatically marked as stale because it has not had 16 | recent activity. It will be closed if no further activity occurs, please comment if you would like this issue to remain open. Thank you 17 | for your contributions. 18 | # Comment to post when closing a stale issue. Set to `false` to disable 19 | closeComment: false 20 | -------------------------------------------------------------------------------- /configuration/testdata/tib_test.conf: -------------------------------------------------------------------------------- 1 | { 2 | "BackEnd": { 3 | "IdentityBackendSettings": { 4 | "Database": 0, 5 | "EnableCluster": false, 6 | "Hosts": { 7 | "localhost": "6379" 8 | }, 9 | "MaxActive": 2000, 10 | "MaxIdle": 1000, 11 | "Password": "" 12 | }, 13 | "Name": "in_memory", 14 | "ProfileBackendSettings": {} 15 | }, 16 | "HttpServerOptions": { 17 | "CertFile": "./certs/server.pem", 18 | "KeyFile": "./certs/server.key", 19 | "UseSSL": false 20 | }, 21 | "Secret": "test-secret", 22 | "TykAPISettings": { 23 | "DashboardConfig": { 24 | "AdminSecret": "12345", 25 | "Endpoint": "http://localhost", 26 | "Port": "3000" 27 | }, 28 | "GatewayConfig": { 29 | "AdminSecret": "54321", 30 | "Endpoint": "http://localhost", 31 | "Port": "80" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tap/ta_provider.go: -------------------------------------------------------------------------------- 1 | /* 2 | package tap wraps a set of interfaces and object to provide a generic interface to a delegated authentication 3 | 4 | proxy 5 | */ 6 | package tap 7 | 8 | import ( 9 | "net/http" 10 | ) 11 | 12 | // TAProvider is an interface that defines an actual handler for a specific authentication provider. It can wrap 13 | // largert libraries (such as Goth for social), or individual pass-throughs such as LDAP. 14 | type TAProvider interface { 15 | Init(IdentityHandler, Profile, []byte) error 16 | Name() string 17 | ProviderType() ProviderType 18 | UseCallback() bool 19 | Handle(http.ResponseWriter, *http.Request, map[string]string, Profile) 20 | HandleCallback(http.ResponseWriter, *http.Request, func(tag string, errorMsg string, rawErr error, code int, w http.ResponseWriter, r *http.Request), Profile) 21 | HandleMetadata(http.ResponseWriter, *http.Request) 22 | } 23 | -------------------------------------------------------------------------------- /internal/jwe/testing.go: -------------------------------------------------------------------------------- 1 | package jwe 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/tls" 7 | 8 | "github.com/go-jose/go-jose/v3" 9 | ) 10 | 11 | func GenerateMockPrivateKey() (*tls.Certificate, error) { 12 | // Generate a new RSA private key for testing 13 | privKey, err := rsa.GenerateKey(rand.Reader, 2048) 14 | if err != nil { 15 | return nil, err 16 | } 17 | cert := &tls.Certificate{ 18 | PrivateKey: privKey, 19 | } 20 | return cert, nil 21 | } 22 | 23 | func CreateJWE(payload []byte, recipient *rsa.PublicKey) (string, error) { 24 | 25 | encrypter, err := jose.NewEncrypter( 26 | jose.A256GCM, 27 | jose.Recipient{ 28 | Algorithm: jose.RSA_OAEP_256, 29 | Key: recipient, 30 | }, 31 | (&jose.EncrypterOptions{}).WithType("JWT")) 32 | if err != nil { 33 | return "", err 34 | } 35 | jwe, err := encrypter.Encrypt(payload) 36 | if err != nil { 37 | return "", err 38 | } 39 | return jwe.CompactSerialize() 40 | } 41 | -------------------------------------------------------------------------------- /tap/identity_handler.go: -------------------------------------------------------------------------------- 1 | /* 2 | package tap wraps a set of interfaces and object to provide a generic interface to a delegated authentication 3 | 4 | proxy 5 | */ 6 | package tap 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/markbates/goth" 12 | ) 13 | 14 | // IdentityHandler provides an interface that provides a generic way to handle the creation / login of an SSO 15 | // session for a specific provider, it should generate users, tokens and SSO sesisons for whatever target system 16 | // is being used off the back of a delegated authentication provider such as GPlus. 17 | type IdentityHandler interface { 18 | Init(interface{}) error 19 | CompleteIdentityAction(http.ResponseWriter, *http.Request, interface{}, Profile) 20 | } 21 | 22 | // GenerateSSOKey is a utility function that creates a temporary ID to identity a user from a delegated provider 23 | func GenerateSSOKey(user goth.User) string { 24 | return user.UserID + "@" + user.Provider + ".com" 25 | } 26 | -------------------------------------------------------------------------------- /tap/action.go: -------------------------------------------------------------------------------- 1 | /* 2 | package tap wraps a set of interfaces and object to provide a generic interface to a delegated authentication 3 | 4 | proxy 5 | */ 6 | package tap 7 | 8 | // An Action is a value that defines what a particular authentication profile will do, for example, create and 9 | // log in a user to the dashboard, or to the portal. Alternatively, create a token or OAuth session 10 | type Action string 11 | 12 | const ( 13 | // Pass through / redirect user-based actions 14 | GenerateOrLoginDeveloperProfile Action = "GenerateOrLoginDeveloperProfile" // Portal 15 | GenerateOrLoginUserProfile Action = "GenerateOrLoginUserProfile" // Dashboard 16 | GenerateOAuthTokenForClient Action = "GenerateOAuthTokenForClient" // OAuth token flow 17 | 18 | // Direct or redirect 19 | GenerateTemporaryAuthToken Action = "GenerateTemporaryAuthToken" // Tyk Access Token 20 | GenerateOAuthTokenForPassword Action = "GenerateOAuthTokenForClient" // OAuth PW flow 21 | ) 22 | -------------------------------------------------------------------------------- /tib_sample.conf: -------------------------------------------------------------------------------- 1 | { 2 | "Secret":"test-secret", 3 | "HttpServerOptions":{ 4 | "UseSSL":false, 5 | "CertFile":"./certs/server.pem", 6 | "KeyFile":"./certs/server.key" 7 | }, 8 | "BackEnd":{ 9 | "Name":"in_memory", 10 | "ProfileBackendSettings":{ 11 | 12 | }, 13 | "IdentityBackendSettings":{ 14 | "Hosts":{ 15 | "localhost":"6379" 16 | }, 17 | "Password":"", 18 | "Database":0, 19 | "EnableCluster":false, 20 | "MaxIdle":1000, 21 | "MaxActive":2000 22 | } 23 | }, 24 | "TykAPISettings":{ 25 | "GatewayConfig":{ 26 | "Endpoint":"http://localhost", 27 | "Port":"80", 28 | "AdminSecret":"54321" 29 | }, 30 | "DashboardConfig":{ 31 | "Endpoint":"http://localhost", 32 | "Port":"3000", 33 | "AdminSecret":"12345" 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:buster-slim 2 | 3 | ARG TYKVERSION 4 | ARG REPOSITORY 5 | LABEL Description="Tyk Identity Broker docker image" Vendor="Tyk" Version=$TYKVERSION 6 | 7 | RUN apt-get update \ 8 | && apt-get upgrade -y \ 9 | && apt-get install -y --no-install-recommends \ 10 | curl ca-certificates apt-transport-https debian-archive-keyring gnupg \ 11 | && curl -L https://packagecloud.io/tyk/$REPOSITORY/gpgkey | apt-key add - \ 12 | && apt-get purge -y gnupg \ 13 | && apt-get autoremove -y \ 14 | && rm -rf /root/.cache 15 | 16 | RUN echo "deb https://packagecloud.io/tyk/$REPOSITORY/debian/ jessie main" | tee /etc/apt/sources.list.d/tyk_tyk-identity-broker.list \ 17 | && apt-get update \ 18 | && apt-get install -y tyk-identity-broker=$TYKVERSION \ 19 | && rm -rf /var/lib/apt/lists/* 20 | 21 | COPY ./tib_sample.conf /opt/tyk-identity-broker/tib.conf 22 | 23 | WORKDIR /opt/tyk-identity-broker 24 | 25 | CMD ["/opt/tyk-identity-broker/tyk-identity-broker", "-c", "/opt/tyk-identity-broker/tib.conf"] 26 | -------------------------------------------------------------------------------- /docker/hooks/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | echo "$PWD" 5 | 6 | echo "CACHE_TAG: $CACHE_TAG" 7 | echo "IMAGE_NAME: $IMAGE_NAME" 8 | echo "DOCKER_REPO: $DOCKER_REPO" 9 | echo "DOCKER_TAG: $DOCKER_TAG" 10 | 11 | dockerfilepath="$PWD/Dockerfile" 12 | repopath="$(dirname $PWD)" 13 | versionfile="$(cat $repopath/version.go)" 14 | regex="v([0-9]+\.[0-9]+\.[0-9]+)" 15 | if [[ $versionfile =~ $regex ]]; then 16 | TYKVERSION=${BASH_REMATCH[1]} 17 | else 18 | echo "No version found in the version file, terminating" 19 | exit 1 20 | fi 21 | 22 | REPOSITORY="tyk-identity-broker" 23 | imagetag=${IMAGE_NAME##*:} 24 | if [[ $imagetag == "unstable" ]]; then 25 | REPOSITORY="$REPOSITORY-unstable" 26 | TYKVERSION="*" # Pick latest version from unstable repo 27 | fi 28 | 29 | 30 | echo "Executing custom build hook for version $TYKVERSION from $REPOSITORY packagecloud repo" 31 | docker build --build-arg TYKVERSION=$TYKVERSION --build-arg REPOSITORY=$REPOSITORY -f $dockerfilepath -t $IMAGE_NAME $repopath 32 | 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: buger 7 | 8 | --- 9 | 10 | **Branch/Environment** 11 | - Branch: [e.g. Master/Release/Stable/Feature branch] 12 | - Environment: [e.g. On-prem/Hybrid/MDCB] 13 | 14 | **Describe the bug** 15 | A clear and concise description of what the bug is. 16 | 17 | **Reproduction steps** 18 | Steps to reproduce the behavior: 19 | 1. Go to '...' 20 | 2. Click on '....' 21 | 3. Scroll down to '....' 22 | 4. See error 23 | 24 | **Actual behavior** 25 | A clear and concise description of what you expected to happen. 26 | 27 | **Expected behavior** 28 | A clear and concise description of what you expected to happen. 29 | 30 | **Screenshots/Video** 31 | If applicable, add screenshots or video to help explain your problem. 32 | 33 | **Browser/Os (please complete the following information):** 34 | - OS: [e.g. iOS] 35 | - Browser [e.g. chrome, safari] 36 | - Version [e.g. 22] 37 | 38 | **Additional context** 39 | Add any other context about the problem here. 40 | -------------------------------------------------------------------------------- /error/error.go: -------------------------------------------------------------------------------- 1 | package error 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | logger "github.com/TykTechnologies/tyk-identity-broker/log" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | var log = logger.Get() 13 | 14 | // APIErrorMessage is an object that defines when a generic error occurred 15 | type APIErrorMessage struct { 16 | Status string 17 | Error string 18 | } 19 | 20 | // HandleError is a generic error handler 21 | func HandleError(tag string, errorMsg string, rawErr error, code int, w http.ResponseWriter, r *http.Request) { 22 | log.WithFields(logrus.Fields{ 23 | "prefix": tag, 24 | "errorMsg": errorMsg, 25 | }).Error(rawErr) 26 | 27 | errorObj := APIErrorMessage{"error", errorMsg} 28 | responseMsg, err := json.Marshal(&errorObj) 29 | 30 | if err != nil { 31 | log.WithField("prefix", tag).Error("[Error Handler] Couldn't marshal error stats: ", err) 32 | fmt.Fprintf(w, "System Error") 33 | return 34 | } 35 | 36 | w.Header().Set("Content-Type", "application/json") 37 | w.WriteHeader(code) 38 | fmt.Fprintf(w, string(responseMsg)) 39 | } 40 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/sirupsen/logrus" 8 | prefixed "github.com/x-cray/logrus-prefixed-formatter" 9 | ) 10 | 11 | var log = logrus.New() 12 | var rawLog = logrus.New() 13 | 14 | type RawFormatter struct{} 15 | 16 | func (f *RawFormatter) Format(entry *logrus.Entry) ([]byte, error) { 17 | return []byte(entry.Message), nil 18 | } 19 | 20 | func init() { 21 | formatter := new(prefixed.TextFormatter) 22 | formatter.TimestampFormat = `Jan 02 15:04:05` 23 | formatter.FullTimestamp = true 24 | 25 | log.Formatter = formatter 26 | rawLog.Formatter = new(RawFormatter) 27 | } 28 | 29 | func Get() *logrus.Logger { 30 | switch strings.ToLower(os.Getenv("TYK_LOGLEVEL")) { 31 | case "error": 32 | log.SetLevel(logrus.ErrorLevel) 33 | case "warn": 34 | log.SetLevel(logrus.WarnLevel) 35 | case "debug": 36 | log.SetLevel(logrus.DebugLevel) 37 | default: 38 | log.SetLevel(logrus.InfoLevel) 39 | } 40 | return log 41 | } 42 | 43 | func SetLogger(logger *logrus.Logger) { 44 | log = logger 45 | } 46 | 47 | func GetRaw() *logrus.Logger { 48 | return rawLog 49 | } 50 | -------------------------------------------------------------------------------- /ci/Dockerfile.std: -------------------------------------------------------------------------------- 1 | # Generated by: gromit policy 2 | 3 | FROM debian:bookworm-slim 4 | ARG TARGETARCH 5 | 6 | ENV DEBIAN_FRONTEND=noninteractive 7 | 8 | RUN apt-get update \ 9 | && apt-get dist-upgrade -y ca-certificates 10 | 11 | # Remove some things to decrease CVE surface 12 | RUN dpkg --purge --force-remove-essential curl ncurses-base || true 13 | RUN rm -fv /usr/bin/passwd /usr/sbin/adduser || true 14 | 15 | # Clean up caches, unwanted .a and .o files 16 | RUN rm -rf /root/.cache \ 17 | && apt-get -y autoremove \ 18 | && apt-get clean \ 19 | && rm -rf /usr/include/* /var/cache/apt/archives /var/lib/{apt,dpkg,cache,log} \ 20 | && find /usr/lib -type f -name '*.a' -o -name '*.o' -delete 21 | 22 | # Comment this to test in dev 23 | COPY *${TARGETARCH}.deb / 24 | RUN rm -f /*fips*.deb && dpkg -i /tyk-identity-broker*${TARGETARCH}.deb && rm /*.deb 25 | 26 | ARG PORTS 27 | 28 | EXPOSE $PORTS 29 | 30 | WORKDIR /opt/tyk-identity-broker/ 31 | 32 | # Uncomment this to test in dev 33 | # COPY tyk-identity-broker . 34 | ENTRYPOINT ["/opt/tyk-identity-broker/tyk-identity-broker" ] 35 | CMD [ "--conf=/opt/tyk-identity-broker/tib.conf" ] 36 | -------------------------------------------------------------------------------- /ci/bin/dist_push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo This is deprecated. Use the goreleaser based automation 4 | 5 | : ${ORGDIR:="/src/github.com/TykTechnologies"} 6 | : ${SOURCEBINPATH:="${ORGDIR}/tyk-identity-broker"} 7 | : ${DEBVERS:="ubuntu/trusty ubuntu/xenial ubuntu/bionic debian/jessie debian/stretch debian/buster"} 8 | : ${RPMVERS:="el/6 el/7"} 9 | : ${PKGNAME:="tyk-identity-broker"} 10 | 11 | echo "Set version number" 12 | : ${VERSION:=$(perl -n -e'/v(\d+).(\d+).(\d+)/'' && print "$1\.$2\.$3"' version.go)} 13 | 14 | RELEASE_DIR="$SOURCEBINPATH/build" 15 | export PACKAGECLOUDREPO=$PC_TARGET 16 | 17 | cd $RELEASE_DIR/ 18 | 19 | for arch in i386 amd64 arm64 20 | do 21 | debName="${PKGNAME}_${VERSION}_${arch}.deb" 22 | rpmName="${PKGNAME}-$VERSION-1.${arch/amd64/x86_64}.rpm" 23 | 24 | for ver in $DEBVERS 25 | do 26 | echo "Pushing $debName to PackageCloud $ver" 27 | package_cloud push tyk/$PACKAGECLOUDREPO/$ver $debName 28 | done 29 | 30 | for ver in $RPMVERS 31 | do 32 | echo "Pushing $rpmName to PackageCloud $ver" 33 | package_cloud push tyk/$PACKAGECLOUDREPO/$ver $rpmName 34 | done 35 | done 36 | -------------------------------------------------------------------------------- /.github/workflows/release-tests.yml: -------------------------------------------------------------------------------- 1 | name: Smoke Tests 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | smoke-tests: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 1 14 | 15 | - uses: aws-actions/configure-aws-credentials@v4 16 | with: 17 | role-to-assume: arn:aws:iam::754489498669:role/ecr_rw_tyk 18 | role-session-name: cipush 19 | aws-region: eu-central-1 20 | 21 | - id: ecr 22 | uses: aws-actions/amazon-ecr-login@v2 23 | with: 24 | mask-password: 'true' 25 | 26 | - name: Run smoke tests 27 | shell: bash 28 | working-directory: ci 29 | run: | 30 | set -eaxo pipefail 31 | if [ ! -d smoke-tests ]; then 32 | echo "::warning No repo specific smoke tests defined" 33 | exit 0 34 | fi 35 | for d in smoke-tests/*/ 36 | do 37 | echo Attempting to test $d 38 | if [ -d $d ] && [ -e $d/test.sh ]; then 39 | cd $d 40 | ./test.sh ${{ steps.ecr.outputs.registry }}/tyk-identity-broker:sha-${{ github.sha }} 41 | cd - 42 | fi 43 | done 44 | -------------------------------------------------------------------------------- /ci/install/inits/upstart/init/1.x/tyk-identity-broker.conf: -------------------------------------------------------------------------------- 1 | description "Tyk Identity Broker" 2 | start on filesystem or runlevel [2345] 3 | stop on runlevel [!2345] 4 | 5 | respawn 6 | umask 022 7 | #nice 8 | #chroot / 9 | chdir /opt/tyk-identity-broker 10 | #limit core 11 | #limit cpu 12 | #limit data 13 | #limit fsize 14 | #limit memlock 15 | #limit msgqueue 16 | #limit nice 17 | #limit nofile 18 | #limit nproc 19 | #limit rss 20 | #limit rtprio 21 | #limit sigpending 22 | #limit stack 23 | setuid tyk 24 | setgid tyk 25 | console log # log stdout/stderr to /var/log/upstart/ 26 | 27 | script 28 | # When loading default and sysconfig files, we use `set -a` to make 29 | # all variables automatically into environment variables. 30 | set -a 31 | [ -r /etc/default/tyk-identity-broker ] && . /etc/default/tyk-identity-broker 32 | [ -r /etc/sysconfig/tyk-identity-broker ] && . /etc/sysconfig/tyk-identity-broker 33 | set +a 34 | exec /opt/tyk-identity-broker/tyk-identity-broker -c /opt/tyk-identity-broker/tib.conf 35 | end script 36 | -------------------------------------------------------------------------------- /ci/bin/unlock-agent.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Generated by: gromit policy 4 | 5 | # Get the GPG fingerprint with gpg --with-keygrip --list-secret-keys 6 | if [[ -z "${PKG_SIGNING_KEY}" || -z "${NFPM_PASSPHRASE}" || -z "${GPG_FINGERPRINT}" ]]; then 7 | echo "No private key set, packages cannnot be signed. Set PKG_SIGNING_KEY, NFPM_PASSPHRASE and GPG_FINGERPRINT" 8 | exit 1 9 | fi 10 | 11 | echo Configuring gpg-agent to accept a passphrase 12 | mkdir ~/.gnupg && chmod 700 ~/.gnupg 13 | cat > ~/.gnupg/gpg-agent.conf < ~/.gnupg/gpg.conf < tyk.io.signing.key 33 | 34 | chmod 400 tyk.io.signing.key 35 | # archive signing can work with gpg 36 | /usr/lib/gnupg2/gpg-preset-passphrase --passphrase $NFPM_PASSPHRASE --preset $GPG_FINGERPRINT 37 | gpg --import --batch --yes tyk.io.signing.key || ( cat /gpg-agent.log; exit 1 ) 38 | -------------------------------------------------------------------------------- /data_loader/mongo_loader_test.go: -------------------------------------------------------------------------------- 1 | package data_loader 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "go.mongodb.org/mongo-driver/mongo" 8 | "gopkg.in/mgo.v2" 9 | ) 10 | 11 | // testCase represents a structure for test cases 12 | type testCase struct { 13 | name string 14 | errorToPass error 15 | expectError bool 16 | } 17 | 18 | // TestHandleEmptyProfilesError tests the handleEmptyProfilesError function using a matrix of test cases. 19 | func TestHandleEmptyProfilesError(t *testing.T) { 20 | testCases := []testCase{ 21 | { 22 | name: "Nil Error", 23 | errorToPass: nil, 24 | expectError: false, 25 | }, 26 | { 27 | name: "mongo: no documents in result", 28 | errorToPass: mongo.ErrNoDocuments, 29 | expectError: false, 30 | }, 31 | { 32 | name: "not found", 33 | errorToPass: mgo.ErrNotFound, 34 | expectError: false, 35 | }, 36 | { 37 | name: "Other Error", 38 | errorToPass: errors.New("some other error"), 39 | expectError: true, 40 | }, 41 | } 42 | 43 | for _, tc := range testCases { 44 | t.Run(tc.name, func(t *testing.T) { 45 | err := handleEmptyProfilesError(tc.errorToPass) 46 | 47 | isErr := err != nil 48 | if isErr != tc.expectError { 49 | t.Errorf("Test '%s' failed: Expected error: %v, Got error: %v", tc.name, tc.expectError, err) 50 | } 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ci/install/inits/upstart/init/0.x/tyk-identity-broker.conf: -------------------------------------------------------------------------------- 1 | description "Tyk Identity Broker" 2 | start on filesystem or runlevel [2345] 3 | stop on runlevel [!2345] 4 | 5 | respawn 6 | umask 022 7 | #nice 8 | #chroot / 9 | chdir /opt/tyk-identity-broker 10 | #limit core 11 | #limit cpu 12 | #limit data 13 | #limit fsize 14 | #limit memlock 15 | #limit msgqueue 16 | #limit nice 17 | #limit nofile 18 | #limit nproc 19 | #limit rss 20 | #limit rtprio 21 | #limit sigpending 22 | #limit stack 23 | 24 | script 25 | # When loading default and sysconfig files, we use `set -a` to make 26 | # all variables automatically into environment variables. 27 | set -a 28 | [ -r /etc/default/tyk-identity-broker ] && . /etc/default/tyk-identity-broker 29 | [ -r /etc/sysconfig/tyk-identity-broker ] && . /etc/sysconfig/tyk-identity-broker 30 | set +a 31 | exec chroot --userspec tyk:tyk / sh -c "cd /opt/tyk-identity-broker; exec /opt/tyk-identity-broker/tyk-identity-broker -c /opt/tyk-identity-broker/tib.conf" >> /var/log/tyk-identity-broker.stdout 2>> /var/log/tyk-identity-broker.stderr 32 | end script 33 | -------------------------------------------------------------------------------- /internal/jwe/JWE.go: -------------------------------------------------------------------------------- 1 | package jwe 2 | 3 | import ( 4 | "crypto/rsa" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/go-jose/go-jose/v3" 10 | 11 | "github.com/markbates/goth/providers/openidConnect" 12 | ) 13 | 14 | type Handler struct { 15 | Enabled bool `json:"Enabled"` 16 | PrivateKeyLocation string `json:"PrivateKeyLocation"` 17 | Key *tls.Certificate `json:"-"` 18 | } 19 | 20 | func (handler *Handler) Decrypt(token string) (string, error) { 21 | if !handler.Enabled { 22 | return token, nil 23 | } 24 | 25 | if handler.Key == nil { 26 | return "", errors.New("JWE Private Key not loaded") 27 | } 28 | 29 | privateKey := handler.Key.PrivateKey.(*rsa.PrivateKey) 30 | 31 | // Parse the serialized token 32 | jwe, err := jose.ParseEncrypted(token) 33 | if err != nil { 34 | return "", fmt.Errorf("error parsing JWE: %v", err) 35 | } 36 | 37 | // Decrypt the token 38 | decrypted, err := jwe.Decrypt(privateKey) 39 | if err != nil { 40 | return "", fmt.Errorf("error decrypting JWE: %v", err) 41 | } 42 | 43 | return string(decrypted), nil 44 | } 45 | 46 | func DecryptIDToken(jweHandler *Handler, JWTSession *openidConnect.Session) error { 47 | decryptedIDToken, err := jweHandler.Decrypt(JWTSession.IDToken) 48 | if err != nil { 49 | return err 50 | } 51 | JWTSession.IDToken = decryptedIDToken 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /ci/install/post_remove.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Generated by: gromit policy 4 | 5 | 6 | cleanRemove() { 7 | printf "\033[32m Post remove for plain removal\033[0m\n" 8 | if command -V systemctl >/dev/null 2>&1; then 9 | systemctl stop tyk-identity-broker ||: 10 | systemctl daemon-reload ||: 11 | fi 12 | service stop tyk-identity-broker ||: 13 | if command -V chkconfig >/dev/null 2>&1; then 14 | chkconfig --del tyk-identity-broker ||: 15 | fi 16 | if command -V update-rc.d >/dev/null 2>&1; then 17 | update-rc.d tyk-identity-broker remove 18 | fi 19 | } 20 | 21 | upgrade() { 22 | printf "\033[32m Post remove for upgrade, nothing to do.\033[0m\n" 23 | } 24 | 25 | action="$1" 26 | if [ "$1" = "configure" ] && [ -z "$2" ]; then 27 | # Alpine linux does not pass args, and deb passes $1=configure 28 | action="install" 29 | elif [ "$1" = "configure" ] && [ -n "$2" ]; then 30 | # deb passes $1=configure $2= 31 | action="upgrade" 32 | fi 33 | 34 | case "$action" in 35 | "1" | "install") 36 | printf "\033[32m Post Install of a clean install\033[0m\n" 37 | cleanRemove 38 | ;; 39 | "2" | "upgrade") 40 | printf "\033[32m Post Install of an upgrade\033[0m\n" 41 | upgrade 42 | ;; 43 | *) 44 | # $1 == version being installed 45 | printf "\033[32m Alpine\033[0m" 46 | cleanRemove 47 | ;; 48 | esac 49 | -------------------------------------------------------------------------------- /toth/toth.go: -------------------------------------------------------------------------------- 1 | /* 2 | package toth is a clone of goth, but modified for multi-tenant usage instead of using 3 | 4 | globals everywhere 5 | */ 6 | package toth 7 | 8 | import ( 9 | "fmt" 10 | 11 | "github.com/markbates/goth" 12 | ) 13 | 14 | // TothInstance wraps a goth configuration 15 | type TothInstance struct { 16 | providers goth.Providers 17 | } 18 | 19 | // Init just creates the basic configuration objects 20 | func (t *TothInstance) Init() { 21 | t.providers = goth.Providers{} 22 | } 23 | 24 | // UseProviders sets a list of available providers for use with Goth. 25 | func (t *TothInstance) UseProviders(viders ...goth.Provider) { 26 | for _, provider := range viders { 27 | t.providers[provider.Name()] = provider 28 | } 29 | } 30 | 31 | // GetProviders returns a list of all the providers currently in use. 32 | func (t *TothInstance) GetProviders() goth.Providers { 33 | return t.providers 34 | } 35 | 36 | // GetProvider returns a previously created provider. If Goth has not 37 | // been told to use the named provider it will return an error. 38 | func (t *TothInstance) GetProvider(name string) (goth.Provider, error) { 39 | provider := t.providers[name] 40 | if provider == nil { 41 | return nil, fmt.Errorf("no provider for %s exists", name) 42 | } 43 | return provider, nil 44 | } 45 | 46 | // ClearProviders will remove all providers currently in use. 47 | // This is useful, mostly, for testing purposes. 48 | func (t *TothInstance) ClearProviders() { 49 | t.providers = goth.Providers{} 50 | } 51 | -------------------------------------------------------------------------------- /internal/jwe/testing_test.go: -------------------------------------------------------------------------------- 1 | package jwe 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "testing" 7 | ) 8 | 9 | func TestGenerateMockPrivateKey(t *testing.T) { 10 | cert, err := GenerateMockPrivateKey() 11 | 12 | // Assert that there is no error 13 | if err != nil { 14 | t.Fatalf("Expected no error, got %v", err) 15 | } 16 | 17 | // Assert that the certificate is not nil 18 | if cert == nil { 19 | t.Fatal("Expected certificate to be non-nil") 20 | } 21 | 22 | // Assert that the PrivateKey is of the correct type 23 | if _, ok := cert.PrivateKey.(*rsa.PrivateKey); !ok { 24 | t.Fatalf("Expected PrivateKey to be of type *rsa.PrivateKey, got %T", cert.PrivateKey) 25 | } 26 | 27 | // Additional check (optional): Verify the size of the generated private key 28 | if cert.PrivateKey.(*rsa.PrivateKey).Size() != 256 { // 2048 bits = 256 bytes 29 | t.Fatalf("Expected private key size to be 256 bytes, got %d", cert.PrivateKey.(*rsa.PrivateKey).Size()) 30 | } 31 | } 32 | 33 | func TestCreateJWE(t *testing.T) { 34 | // Generate a new RSA key pair for testing 35 | privKey, err := rsa.GenerateKey(rand.Reader, 2048) 36 | if err != nil { 37 | t.Fatalf("Failed to generate RSA key pair: %v", err) 38 | } 39 | publicKey := &privKey.PublicKey 40 | 41 | // Define a sample payload 42 | payload := []byte("test payload") 43 | 44 | // Call the CreateJWE function 45 | jwe, err := CreateJWE(payload, publicKey) 46 | 47 | // Assert that there is no error 48 | if err != nil { 49 | t.Fatalf("Expected no error, got %v", err) 50 | } 51 | 52 | // Assert that the JWE string is not empty 53 | if jwe == "" { 54 | t.Fatal("Expected JWE to be non-empty") 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /bin/ci-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | export GO111MODULE=on 6 | 7 | # print a command and execute it 8 | show() { 9 | echo "$@" >&2 10 | eval "$@" 11 | } 12 | 13 | fatal() { 14 | echo "$@" >&2 15 | exit 1 16 | } 17 | 18 | TEST_TIMEOUT=10m 19 | db="file" 20 | 21 | if [[ -n $1 ]]; then 22 | db=$1 23 | fi 24 | 25 | if [[ $db = "mongo-mgo" ]]; then 26 | export TYK_IB_STORAGE_STORAGETYPE="mongo" 27 | export TYK_IB_STORAGE_MONGOCONF_MONGOURL="mongodb://localhost/tyk_identity_broker" 28 | export TYK_IB_STORAGE_MONGOCONF_DRIVER="mgo" 29 | : # do nothing 30 | elif [[ $db = "mongo-official" ]]; then 31 | export TYK_IB_STORAGE_STORAGETYPE="mongo" 32 | export TYK_IB_STORAGE_MONGOCONF_MONGOURL="mongodb://localhost/tyk_identity_broker" 33 | export TYK_IB_STORAGE_MONGOCONF_DRIVER="mongo-go" 34 | elif [[ $db = "file" ]]; then 35 | export TYK_IB_STORAGE_STORAGETYPE="file" 36 | export TYK_IB_PROFILEDIR="./" 37 | else 38 | fatal "unsupported database: $db" 39 | fi 40 | 41 | echo "Running with $TYK_IB_STORAGE_STORAGETYPE database using $TYK_IB_STORAGE_MONGOCONF_DRIVER driver" 42 | 43 | show go vet . || fatal "go vet errored" 44 | 45 | GO_FILES=$(find * -name '*.go' ) 46 | 47 | echo "Formatting checks..." 48 | 49 | FMT_FILES="$(gofmt -s -l ${GO_FILES})" 50 | if [[ -n ${FMT_FILES} ]]; then 51 | fatal "Run 'gofmt -s -w' on these files:\n$FMT_FILES" 52 | fi 53 | 54 | echo "gofmt check is ok!" 55 | 56 | IMP_FILES="$(goimports -l ${GO_FILES})" 57 | if [[ -n ${IMP_FILES} ]]; then 58 | fatal "Run 'goimports -w' on these files:\n$IMP_FILES" 59 | fi 60 | 61 | echo "goimports check is ok!" 62 | 63 | for pkg in $(go list github.com/TykTechnologies/tyk-identity-broker/...); 64 | do 65 | race="-race" 66 | echo "Testing... $pkg" 67 | coveragefile=`echo "$pkg-$db" | awk -F/ '{print $NF}'` 68 | show go test -timeout ${TEST_TIMEOUT} ${race} --coverprofile=${coveragefile}.cov -v ${pkg} 69 | done 70 | -------------------------------------------------------------------------------- /data_loader/data_loader_test.go: -------------------------------------------------------------------------------- 1 | package data_loader 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/TykTechnologies/storage/persistent" 8 | "github.com/TykTechnologies/tyk-identity-broker/backends" 9 | "github.com/stretchr/testify/assert" 10 | 11 | "github.com/TykTechnologies/tyk-identity-broker/configuration" 12 | ) 13 | 14 | func TestCreateDataMongoLoader(t *testing.T) { 15 | isMongo := isMongoEnv() 16 | if !isMongo { 17 | t.Skip() 18 | } 19 | 20 | url, driver := MongoEnvConf() 21 | 22 | conf := configuration.Configuration{ 23 | Storage: &configuration.Storage{ 24 | StorageType: "mongo", 25 | MongoConf: &configuration.MongoConf{ 26 | MongoURL: url, 27 | Driver: driver, 28 | }, 29 | }, 30 | } 31 | 32 | dataLoader, err := CreateDataLoader(conf, "") 33 | if err != nil { 34 | t.Fatalf("creating Mongo data loader: %v", err) 35 | } 36 | 37 | if _, ok := dataLoader.(*MongoLoader); !ok { 38 | t.Fatalf("type of data loader is not correct; expected '*data_loader.MongoLoader' but got '%T'", dataLoader) 39 | } 40 | } 41 | 42 | func TestFlush(t *testing.T) { 43 | 44 | isMongo := isMongoEnv() 45 | if !isMongo { 46 | t.Skip() 47 | } 48 | 49 | url, driver := MongoEnvConf() 50 | loader := MongoLoader{} 51 | 52 | var err error 53 | loader.store, err = persistent.NewPersistentStorage(&persistent.ClientOpts{ 54 | ConnectionString: url, 55 | UseSSL: false, 56 | Type: driver, 57 | }) 58 | 59 | assert.Nil(t, err) 60 | 61 | authStore := &backends.InMemoryBackend{} 62 | err = loader.Flush(authStore) 63 | assert.Nil(t, err) 64 | } 65 | 66 | func isMongoEnv() bool { 67 | storageType := os.Getenv("TYK_IB_STORAGE_STORAGETYPE") 68 | 69 | // Check if MongoDB is the storage type and both URL and driver are set 70 | return storageType == "mongo" 71 | } 72 | 73 | func MongoEnvConf() (string, string) { 74 | mongoURL := os.Getenv("TYK_IB_STORAGE_MONGOCONF_MONGOURL") 75 | mongoDriver := os.Getenv("TYK_IB_STORAGE_MONGOCONF_DRIVER") 76 | 77 | return mongoURL, mongoDriver 78 | 79 | } 80 | -------------------------------------------------------------------------------- /ci/bin/pc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Generated by: gromit policy 4 | 5 | # goreleaser calls a custom publisher for each artefact packagecloud 6 | # expects the distro version when pushing this script bridges both by 7 | # choosing the appropriate list of distro versions from $DEBVERS and 8 | # $RPMVERS 9 | # $REPO, $DEBVERS and $RPMVERS are expected to be set by the 10 | # user 11 | 12 | REQUIRED_VARS="PACKAGECLOUD_TOKEN REPO" 13 | 14 | usage() { 15 | cat <" \ 57 | --define "%__gpg /usr/bin/gpg" \ 58 | --addsign $pkg 59 | fi 60 | ;; 61 | *) 62 | echo "Unknown package, not uploading" 63 | esac 64 | 65 | for i in $vers; do 66 | 67 | [[ ! -s ${pkg} ]] && echo "File is empty or does not exists" && exit 1 68 | 69 | # Yank packages first to enable tag re-use 70 | packagecloud rm $REPO/$i $(basename $pkg) || true 71 | packagecloud push $REPO/$i $pkg 72 | 73 | done 74 | -------------------------------------------------------------------------------- /tap/identity-handlers/dummy_handler.go: -------------------------------------------------------------------------------- 1 | /* 2 | package identityHandlers provides a collection of handlers for target systems, 3 | 4 | these handlers create accounts and sso tokens 5 | */ 6 | package identityHandlers 7 | 8 | import ( 9 | "fmt" 10 | "net/http" 11 | "sync" 12 | 13 | logger "github.com/TykTechnologies/tyk-identity-broker/log" 14 | "github.com/TykTechnologies/tyk-identity-broker/tap" 15 | "github.com/sirupsen/logrus" 16 | ) 17 | 18 | var log = logger.Get() 19 | var DummyLogTag string = "[DUMMY ID HANDLER]" 20 | var onceReloadDummyLogger sync.Once 21 | var dummyLogger = log.WithField("prefix", DummyLogTag) 22 | 23 | // DummyIdentityHandler is a dummy hndler, use for testing 24 | type DummyIdentityHandler struct{} 25 | 26 | // Init will set up the configuration of the handler 27 | func (d DummyIdentityHandler) Init(conf interface{}) error { 28 | //if an external logger was set, then lets reload it to inherit those configs 29 | onceReloadDummyLogger.Do(func() { 30 | log = logger.Get() 31 | dummyLogger = &logrus.Entry{Logger: log} 32 | dummyLogger = dummyLogger.Logger.WithField("prefix", DummyLogTag) 33 | }) 34 | return nil 35 | } 36 | 37 | // Dummy method 38 | func (d DummyIdentityHandler) CreateIdentity(i interface{}) (string, error) { 39 | dummyLogger.Info("Creating identity for: ", i) 40 | return "", nil 41 | } 42 | 43 | // Dummy method 44 | func (d DummyIdentityHandler) LoginIdentity(user string, pass string) (string, error) { 45 | dummyLogger.Info("Logging in identity: ", user) 46 | return "12345", nil 47 | } 48 | 49 | // CompleteIdentityAction is called when an authenticated callback event is triggered, it should speak to 50 | // the target system and generate / login the user. In this case it redirects the user to the ReturnURL. 51 | func (d DummyIdentityHandler) CompleteIdentityAction(w http.ResponseWriter, r *http.Request, i interface{}, profile tap.Profile) { 52 | d.CreateIdentity(i) 53 | nonce, _ := d.LoginIdentity("DUMMY", "DUMMY") 54 | 55 | // After login, we need to redirect this user 56 | dummyLogger.Debug("--> Running redirect...") 57 | if profile.ReturnURL != "" { 58 | newURL := profile.ReturnURL + "?nonce=" + nonce 59 | http.Redirect(w, r, newURL, 301) 60 | return 61 | } 62 | 63 | dummyLogger.Warning("No return URL found, redirect failed.") 64 | fmt.Fprintf(w, "Success! (Have you set a return URL?)") 65 | } 66 | -------------------------------------------------------------------------------- /data_loader/data_loader.go: -------------------------------------------------------------------------------- 1 | package data_loader 2 | 3 | import ( 4 | "github.com/TykTechnologies/storage/persistent" 5 | "github.com/TykTechnologies/tyk-identity-broker/configuration" 6 | logger "github.com/TykTechnologies/tyk-identity-broker/log" 7 | "github.com/TykTechnologies/tyk-identity-broker/tap" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | var log = logger.Get() 12 | var dataLoaderLoggerTag = "TIB DATA LOADER" 13 | var dataLogger = log.WithField("prefix", dataLoaderLoggerTag) 14 | 15 | // DataLoader is an interface that defines how data is loaded from a source into a AuthRegisterBackend interface store 16 | type DataLoader interface { 17 | Init(conf interface{}) error 18 | LoadIntoStore(tap.AuthRegisterBackend) error 19 | Flush(tap.AuthRegisterBackend) error 20 | } 21 | 22 | func reloadDataLoaderLogger() { 23 | log = logger.Get() 24 | dataLogger = &logrus.Entry{Logger: log} 25 | dataLogger = dataLogger.Logger.WithField("prefix", dataLoaderLoggerTag) 26 | } 27 | 28 | func CreateDataLoader(config configuration.Configuration, ProfileFilename string) (DataLoader, error) { 29 | var dataLoader DataLoader 30 | var loaderConf interface{} 31 | reloadDataLoaderLogger() 32 | 33 | //default storage 34 | storageType := configuration.FILE 35 | 36 | if config.Storage != nil { 37 | storageType = config.Storage.StorageType 38 | } 39 | 40 | switch storageType { 41 | case configuration.MONGO: 42 | dataLoader = &MongoLoader{} 43 | 44 | mongoConf := config.Storage.MongoConf 45 | 46 | mongoDriver := configuration.GetMongoDriver(config.Storage.MongoConf.Driver) 47 | // map from tib mongo conf structure to persistent.ClientOpts 48 | connectionConf := persistent.ClientOpts{ 49 | ConnectionString: mongoConf.MongoURL, 50 | UseSSL: mongoConf.MongoUseSSL, 51 | SSLInsecureSkipVerify: mongoConf.MongoSSLInsecureSkipVerify, 52 | SessionConsistency: mongoConf.SessionConsistency, 53 | Type: mongoDriver, 54 | DirectConnection: mongoConf.DirectConnection, 55 | } 56 | 57 | loaderConf = MongoLoaderConf{ 58 | ClientOpts: &connectionConf, 59 | } 60 | default: 61 | //default: FILE 62 | dataLoader = &FileLoader{} 63 | loaderConf = configuration.FileLoaderConf{ 64 | FileName: ProfileFilename, 65 | ProfileDir: config.ProfileDir, 66 | } 67 | } 68 | 69 | err := dataLoader.Init(loaderConf) 70 | return dataLoader, err 71 | } 72 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Related Issue 7 | 8 | 9 | 10 | 11 | 12 | ## Motivation and Context 13 | 14 | 15 | ## How This Has Been Tested 16 | 17 | 19 | 20 | ## Screenshots (if appropriate) 21 | 22 | ## Types of changes 23 | 24 | - [ ] Bug fix (non-breaking change which fixes an issue) 25 | - [ ] New feature (non-breaking change which adds functionality) 26 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 27 | - [ ] Refactoring or add test (improvements in base code or adds test coverage to functionality) 28 | 29 | ## Checklist 30 | 31 | 32 | - [ ] Make sure you are requesting to **pull a topic/feature/bugfix branch** (right side). If pulling from your own 33 | fork, don't request your `master`! 34 | - [ ] Make sure you are making a pull request against the **`master` branch** (left side). Also, you should start 35 | *your branch* off *our latest `master`*. 36 | - [ ] My change requires a change to the documentation. 37 | - [ ] If you've changed APIs, describe what needs to be updated in the documentation. 38 | - [ ] If new config option added, ensure that it can be set via ENV variable 39 | - [ ] I have updated the documentation accordingly. 40 | - [ ] Modules and vendor dependencies have been updated; run `go mod tidy && go mod vendor` 41 | - [ ] When updating library version must provide reason/explanation for this update. 42 | - [ ] I have added tests to cover my changes. 43 | - [ ] All new and existing tests passed. 44 | - [ ] Check your code additions will not fail linting checks: 45 | - [ ] `go fmt -s` 46 | - [ ] `go vet` 47 | -------------------------------------------------------------------------------- /http_handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/TykTechnologies/tyk-identity-broker/constants" 8 | "github.com/TykTechnologies/tyk-identity-broker/providers" 9 | 10 | tykerrors "github.com/TykTechnologies/tyk-identity-broker/error" 11 | "github.com/gorilla/mux" 12 | ) 13 | 14 | // Returns a profile ID 15 | func getId(req *http.Request) (string, error) { 16 | id := mux.Vars(req)["id"] 17 | if id == "" { 18 | id = mux.Vars(req)[":id"] 19 | } 20 | if id == "" { 21 | return id, errors.New("no profile id detected") 22 | } 23 | return id, nil 24 | 25 | } 26 | 27 | // HandleAuth is the main entry point handler for any profile (i.e. /auth/:profile-id/:provider) 28 | func HandleAuth(w http.ResponseWriter, r *http.Request) { 29 | 30 | thisId, idErr := getId(r) 31 | if idErr != nil { 32 | tykerrors.HandleError(constants.HandlerLogTag, "Could not retrieve ID", idErr, 400, w, r) 33 | return 34 | } 35 | 36 | thisIdentityProvider, thisProfile, err := providers.GetTapProfile(AuthConfigStore, IdentityKeyStore, thisId, TykAPIHandler) 37 | if err != nil { 38 | return 39 | } 40 | 41 | pathParams := mux.Vars(r) 42 | thisIdentityProvider.Handle(w, r, pathParams, thisProfile) 43 | return 44 | } 45 | 46 | // HandleAuthCallback Is a callback URL passed to OAuth providers such as Social, handles completing an auth request 47 | func HandleAuthCallback(w http.ResponseWriter, r *http.Request) { 48 | 49 | thisId, idErr := getId(r) 50 | if idErr != nil { 51 | tykerrors.HandleError(constants.HandlerLogTag, "Could not retrieve ID", idErr, 400, w, r) 52 | return 53 | } 54 | 55 | thisIdentityProvider, thisProfile, err := providers.GetTapProfile(AuthConfigStore, IdentityKeyStore, thisId, TykAPIHandler) 56 | if err != nil { 57 | tykerrors.HandleError(constants.HandlerLogTag, err.Message, err.Error, err.Code, w, r) 58 | return 59 | } 60 | thisIdentityProvider.HandleCallback(w, r, tykerrors.HandleError, thisProfile) 61 | return 62 | } 63 | 64 | func HandleHealthCheck(w http.ResponseWriter, r *http.Request) { 65 | w.WriteHeader(http.StatusOK) 66 | } 67 | 68 | func HandleMetadata(w http.ResponseWriter, r *http.Request) { 69 | thisId, idErr := getId(r) 70 | if idErr != nil { 71 | tykerrors.HandleError(constants.HandlerLogTag, "Could not retrieve ID", idErr, 400, w, r) 72 | return 73 | } 74 | 75 | thisIdentityProvider, _, err := providers.GetTapProfile(AuthConfigStore, IdentityKeyStore, thisId, TykAPIHandler) 76 | if err != nil { 77 | tykerrors.HandleError(constants.HandlerLogTag, err.Message, err.Error, err.Code, w, r) 78 | return 79 | } 80 | thisIdentityProvider.HandleMetadata(w, r) 81 | return 82 | } 83 | -------------------------------------------------------------------------------- /initializer/initializer.go: -------------------------------------------------------------------------------- 1 | package initializer 2 | 3 | import ( 4 | "github.com/TykTechnologies/storage/persistent" 5 | temporal "github.com/TykTechnologies/storage/temporal/keyvalue" 6 | "github.com/TykTechnologies/tyk-identity-broker/backends" 7 | 8 | logger "github.com/TykTechnologies/tyk-identity-broker/log" 9 | "github.com/TykTechnologies/tyk-identity-broker/providers" 10 | "github.com/TykTechnologies/tyk-identity-broker/tap" 11 | "github.com/TykTechnologies/tyk-identity-broker/tothic" 12 | "github.com/TykTechnologies/tyk/certs" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | var log = logger.Get() 17 | var initializerLogger = log.WithField("prefix", "TIB INITIALIZER") 18 | 19 | // initBackend: Get our backend to use from configs files, new back-ends must be registered here 20 | func InitBackend(profileBackendConfiguration interface{}, identityBackendConfiguration interface{}) (tap.AuthRegisterBackend, tap.AuthRegisterBackend) { 21 | 22 | AuthConfigStore := &backends.InMemoryBackend{} 23 | IdentityKeyStore := &backends.RedisBackend{KeyPrefix: "identity-cache."} 24 | 25 | initializerLogger.Info("Initialising Profile Configuration Store") 26 | AuthConfigStore.Init(profileBackendConfiguration) 27 | initializerLogger.Info("Initialising Identity Cache") 28 | IdentityKeyStore.Init(identityBackendConfiguration) 29 | 30 | return AuthConfigStore, IdentityKeyStore 31 | } 32 | 33 | // CreateBackendFromRedisConn: creates a redis backend from an existent redis Connection 34 | func CreateBackendFromRedisConn(kv temporal.KeyValue, keyPrefix string) tap.AuthRegisterBackend { 35 | redisBackend := &backends.RedisBackend{KeyPrefix: keyPrefix} 36 | 37 | initializerLogger.Info("Initializing Identity Cache") 38 | redisBackend.SetDb(kv) 39 | 40 | return redisBackend 41 | } 42 | 43 | func SetLogger(newLogger *logrus.Logger) { 44 | logger.SetLogger(newLogger) 45 | log = newLogger 46 | 47 | initializerLogger = &logrus.Entry{Logger: log} 48 | initializerLogger = initializerLogger.Logger.WithField("prefix", "TIB INITIALIZER") 49 | } 50 | 51 | func SetCertManager(cm certs.CertificateManager) { 52 | providers.CertManager = cm 53 | } 54 | 55 | func CreateInMemoryBackend() tap.AuthRegisterBackend { 56 | inMemoryBackend := &backends.InMemoryBackend{} 57 | var config interface{} 58 | inMemoryBackend.Init(config) 59 | return inMemoryBackend 60 | } 61 | 62 | func CreateMongoBackend(store persistent.PersistentStorage) tap.AuthRegisterBackend { 63 | mongoBackend := &backends.MongoBackend{ 64 | Store: store, 65 | Collection: tap.ProfilesCollectionName, 66 | } 67 | var config interface{} 68 | mongoBackend.Init(config) 69 | return mongoBackend 70 | } 71 | 72 | func SetConfigHandler(backend tap.AuthRegisterBackend) { 73 | tothic.SetParamsStoreHandler(backend) 74 | } 75 | -------------------------------------------------------------------------------- /providers/saml_test.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_ReadNamesFromClaims(t *testing.T) { 10 | var testMatrix = []struct { 11 | rawData map[string]interface{} 12 | ForeNameClaim string 13 | SurNameClaim string 14 | ForeNameExpected string 15 | SurNameExpected string 16 | }{ 17 | // read default claim 18 | { 19 | rawData: map[string]interface{}{ 20 | DefaultForeNameClaim: "jhon", 21 | DefaultSurNameClaim: "doe", 22 | }, 23 | ForeNameClaim: "", 24 | SurNameClaim: "", 25 | ForeNameExpected: "jhon", 26 | SurNameExpected: "doe", 27 | }, 28 | // read custom claim 29 | { 30 | rawData: map[string]interface{}{ 31 | "custom-forename-claim": "jhon", 32 | "custom-surname-claim": "doe", 33 | }, 34 | ForeNameClaim: "custom-forename-claim", 35 | SurNameClaim: "custom-surname-claim", 36 | ForeNameExpected: "jhon", 37 | SurNameExpected: "doe", 38 | }, 39 | // read custom claims that doesnt comes from idp...(bad mapping) 40 | { 41 | rawData: map[string]interface{}{}, 42 | ForeNameClaim: "custom-forename-claim", 43 | SurNameClaim: "custom-surname-claim", 44 | ForeNameExpected: "", 45 | SurNameExpected: "", 46 | }, 47 | } 48 | 49 | for _, ts := range testMatrix { 50 | forename, surname := ReadNamesFromClaims(ts.ForeNameClaim, ts.SurNameClaim, ts.rawData) 51 | assert.Equal(t, ts.ForeNameExpected, forename) 52 | assert.Equal(t, ts.SurNameExpected, surname) 53 | } 54 | } 55 | 56 | func Test_ReadEmailFromClaims(t *testing.T) { 57 | var testMatrix = []struct { 58 | rawData map[string]interface{} 59 | emailClaim string 60 | emailExpected string 61 | }{ 62 | // read default claim 63 | { 64 | rawData: map[string]interface{}{ 65 | DefaultEmailClaim: "jhon@doe.com", 66 | }, 67 | emailExpected: "jhon@doe.com", 68 | }, 69 | // read custom claim 70 | { 71 | rawData: map[string]interface{}{ 72 | "custom-email-claim": "jhon@doe.com", 73 | }, 74 | emailClaim: "custom-email-claim", 75 | emailExpected: "jhon@doe.com", 76 | }, 77 | // read custom claims that doesnt comes from idp...(bad mapping) 78 | { 79 | rawData: map[string]interface{}{}, 80 | emailClaim: "custom-email-claim", 81 | emailExpected: "", 82 | }, 83 | // WIF 84 | { 85 | rawData: map[string]interface{}{ 86 | "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/": "", 87 | WIFUniqueName: "jhon@doe.com", 88 | }, 89 | emailClaim: "", 90 | emailExpected: "jhon@doe.com", 91 | }, 92 | } 93 | 94 | for _, ts := range testMatrix { 95 | email := ReadEmailFromClaims(ts.emailClaim, ts.rawData) 96 | assert.Equal(t, ts.emailExpected, email) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /backends/in_memory.go: -------------------------------------------------------------------------------- 1 | /* 2 | package backends provides different storage back ends for the configuration of a 3 | 4 | TAP node. Backends need to only be k/v stores. The in-memory provider is given as an example and usefule for testing 5 | */ 6 | package backends 7 | 8 | import ( 9 | "encoding/json" 10 | "errors" 11 | "sync" 12 | 13 | "github.com/TykTechnologies/tyk-identity-broker/log" 14 | "github.com/TykTechnologies/tyk-identity-broker/tap" 15 | "github.com/sirupsen/logrus" 16 | ) 17 | 18 | var logger = log.Get() 19 | var inMemoryLogTag = "TIB IN-MEMORY STORE" 20 | var inMemoryLogger = logger.WithField("prefix", inMemoryLogTag) 21 | 22 | // InMemoryBackend implements tap.AuthRegisterBackend to store profile configs in memory 23 | type InMemoryBackend struct { 24 | kv map[string]interface{} 25 | lock sync.RWMutex 26 | } 27 | 28 | // Init will create the initial in-memory store structures 29 | func (m *InMemoryBackend) Init(config interface{}) error { 30 | logger = log.Get() 31 | inMemoryLogger = &logrus.Entry{Logger: logger} 32 | inMemoryLogger = inMemoryLogger.Logger.WithField("prefix", inMemoryLogTag) 33 | 34 | inMemoryLogger.Info("Initialised") 35 | m.kv = make(map[string]interface{}) 36 | m.lock = sync.RWMutex{} 37 | return nil 38 | } 39 | 40 | // SetKey will set the value of a key in the map 41 | func (m *InMemoryBackend) SetKey(key string, orgId string, val interface{}) error { 42 | if m.kv == nil { 43 | return errors.New("store not initialised!") 44 | } 45 | 46 | asByte, encErr := json.Marshal(val) 47 | if encErr != nil { 48 | return encErr 49 | } 50 | 51 | m.lock.Lock() 52 | m.kv[key] = asByte 53 | m.lock.Unlock() 54 | return nil 55 | } 56 | 57 | // SetKey will set the value of a key in the map 58 | func (m *InMemoryBackend) DeleteKey(key, orgId string) error { 59 | m.lock.Lock() 60 | delete(m.kv, key) 61 | m.lock.Unlock() 62 | return nil 63 | } 64 | 65 | // GetKey will retuyrn the value of a key as an interface 66 | func (m *InMemoryBackend) GetKey(key string, orgId string, target interface{}) error { 67 | m.lock.RLock() 68 | defer m.lock.RUnlock() 69 | 70 | v, ok := m.kv[key] 71 | 72 | if !ok { 73 | return errors.New("not found") 74 | } 75 | 76 | decErr := json.Unmarshal(v.([]byte), target) 77 | if decErr != nil { 78 | return decErr 79 | } 80 | 81 | return nil 82 | } 83 | 84 | func (m *InMemoryBackend) GetAll(orgId string) []interface{} { 85 | 86 | m.lock.RLock() 87 | defer m.lock.RUnlock() 88 | 89 | target := make([]interface{}, 0) 90 | for _, v := range m.kv { 91 | 92 | var thisVal tap.Profile 93 | decErr := json.Unmarshal(v.([]byte), &thisVal) 94 | if decErr != nil { 95 | inMemoryLogger.Error(decErr) 96 | } else { 97 | target = append(target, thisVal) 98 | } 99 | } 100 | return target 101 | } 102 | -------------------------------------------------------------------------------- /data_loader/file_loader.go: -------------------------------------------------------------------------------- 1 | package data_loader 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "path" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/TykTechnologies/tyk-identity-broker/configuration" 11 | "github.com/TykTechnologies/tyk-identity-broker/tap" 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // FileLoader implements DataLoader and will load TAP Profiles from a file 16 | type FileLoader struct { 17 | config configuration.FileLoaderConf 18 | } 19 | 20 | // Init initialises the file loader 21 | func (f *FileLoader) Init(conf interface{}) error { 22 | f.config = conf.(configuration.FileLoaderConf) 23 | return nil 24 | } 25 | 26 | // LoadIntoStore will load, unmarshal and copy profiles into a an AuthRegisterBackend 27 | func (f *FileLoader) LoadIntoStore(store tap.AuthRegisterBackend) error { 28 | profiles := []tap.Profile{} 29 | 30 | thisSet, err := ioutil.ReadFile(f.config.FileName) 31 | if err != nil { 32 | dataLogger.WithFields(logrus.Fields{ 33 | "filename": f.config.FileName, 34 | "error": err, 35 | }).Error("Load failure") 36 | return err 37 | } else { 38 | jsErr := json.Unmarshal(thisSet, &profiles) 39 | if jsErr != nil { 40 | dataLogger.WithField("error", jsErr).Error("Couldn't unmarshal profile set") 41 | return err 42 | } 43 | } 44 | 45 | var loaded int 46 | for _, profile := range profiles { 47 | inputErr := store.SetKey(profile.ID, profile.OrgID, profile) 48 | if inputErr != nil { 49 | dataLogger.WithField("error", inputErr).Error("Couldn't encode configuration") 50 | } else { 51 | loaded += 1 52 | } 53 | } 54 | 55 | dataLogger.WithField("filename", f.config.FileName).Infof("Loaded %d profiles", loaded) 56 | return nil 57 | } 58 | 59 | func (f *FileLoader) Flush(store tap.AuthRegisterBackend) error { 60 | oldSet, err := ioutil.ReadFile(f.config.FileName) 61 | if err != nil { 62 | dataLogger.WithFields(logrus.Fields{ 63 | "filename": f.config.FileName, 64 | "error": err, 65 | }).Error("load failed!") 66 | return err 67 | } 68 | 69 | ts := strconv.Itoa(int(time.Now().Unix())) 70 | bkFilename := "profiles_backup_" + ts + ".json" 71 | bkLocation := path.Join(f.config.ProfileDir, bkFilename) 72 | 73 | wErr := ioutil.WriteFile(bkLocation, oldSet, 0644) 74 | if wErr != nil { 75 | dataLogger.WithFields(logrus.Fields{ 76 | "bk_filename": bkFilename, 77 | "error": err, 78 | }).Error("backup failed! ", wErr) 79 | return wErr 80 | } 81 | 82 | newSet := store.GetAll("") 83 | asJson, encErr := json.Marshal(newSet) 84 | if encErr != nil { 85 | dataLogger.WithField("error", encErr).Error("Encoding failed!") 86 | return encErr 87 | } 88 | 89 | savePath := path.Join(f.config.ProfileDir, f.config.FileName) 90 | 91 | w2Err := ioutil.WriteFile(savePath, asJson, 0644) 92 | if wErr != nil { 93 | dataLogger.WithField("error", w2Err).Error("flush failed!") 94 | return w2Err 95 | } 96 | 97 | return nil 98 | 99 | } 100 | -------------------------------------------------------------------------------- /backends/mongo.go: -------------------------------------------------------------------------------- 1 | package backends 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/TykTechnologies/storage/persistent" 8 | "github.com/TykTechnologies/storage/persistent/model" 9 | "github.com/TykTechnologies/tyk-identity-broker/log" 10 | "github.com/TykTechnologies/tyk-identity-broker/tap" 11 | ) 12 | 13 | var mongoPrefix = "mongo-backend" 14 | var mongoLogger = log.Get().WithField("prefix", mongoPrefix).Logger 15 | 16 | type MongoBackend struct { 17 | Store persistent.PersistentStorage 18 | Collection string 19 | } 20 | 21 | func (m MongoBackend) Init(interface{}) error { 22 | return nil 23 | } 24 | 25 | func (m MongoBackend) SetKey(key string, orgId string, value interface{}) error { 26 | 27 | profile := value.(tap.Profile) 28 | filter := model.DBM{} 29 | filter["ID"] = key 30 | if orgId != "" { 31 | filter["OrgID"] = orgId 32 | } 33 | 34 | // delete if exists, where matches the profile ID and org 35 | err := m.Store.Delete(context.Background(), profile, filter) 36 | if err != nil { 37 | if err.Error() != "not found" { 38 | mongoLogger.WithError(err).Error("setting profile in mongo") 39 | } 40 | } 41 | 42 | err = m.Store.Insert(context.Background(), profile) 43 | if err != nil { 44 | mongoLogger.WithError(err).Error("inserting profile in mongo") 45 | } 46 | 47 | return err 48 | } 49 | 50 | func (m MongoBackend) GetKey(key string, orgId string, val interface{}) error { 51 | 52 | filter := model.DBM{} 53 | filter["ID"] = key 54 | if orgId != "" { 55 | filter["OrgID"] = orgId 56 | } 57 | 58 | p := tap.Profile{} 59 | err := m.Store.Query(context.Background(), p, &p, filter) 60 | if err != nil { 61 | if err.Error() != "not found" { 62 | mongoLogger.WithError(err).Error("error reading profile from mongo, key:", key) 63 | } 64 | } 65 | 66 | // Mongo doesn't parse well the nested map[string]interface{} so, we need to use json marshal/unmarshal 67 | // Mongo let those maps as bson.M 68 | data, err := json.Marshal(p) 69 | if err != nil { 70 | mongoLogger.WithError(err).Error("error marshaling profile") 71 | return err 72 | } 73 | 74 | if err := json.Unmarshal(data, &val); err != nil { 75 | mongoLogger.WithError(err).Error("error un-marshaling profile ") 76 | return err 77 | } 78 | 79 | return err 80 | } 81 | 82 | func (m MongoBackend) GetAll(orgId string) []interface{} { 83 | var profiles []tap.Profile 84 | 85 | filter := model.DBM{} 86 | if orgId != "" { 87 | filter["OrgID"] = orgId 88 | } 89 | 90 | err := m.Store.Query(context.Background(), tap.Profile{}, &profiles, filter) 91 | if err != nil { 92 | mongoLogger.Error("error reading profiles from mongo: " + err.Error()) 93 | } 94 | 95 | result := make([]interface{}, len(profiles)) 96 | for i, profile := range profiles { 97 | result[i] = profile 98 | } 99 | 100 | return result 101 | } 102 | 103 | func (m MongoBackend) DeleteKey(key string, orgId string) error { 104 | filter := model.DBM{} 105 | filter["ID"] = key 106 | if orgId != "" { 107 | filter["OrgID"] = orgId 108 | } 109 | 110 | err := m.Store.Delete(context.Background(), tap.Profile{}, filter) 111 | if err != nil { 112 | mongoLogger.WithError(err).Error("removing profile") 113 | } 114 | 115 | return err 116 | } 117 | -------------------------------------------------------------------------------- /providers/util_slug.go: -------------------------------------------------------------------------------- 1 | /* 2 | package providers is a catch-all for all TAP auth provider types (e.g. social, active directory), if you are 3 | 4 | extending TAP to use more providers, add them to this section 5 | */ 6 | package providers 7 | 8 | import ( 9 | "encoding/hex" 10 | "unicode" 11 | "unicode/utf8" 12 | 13 | "golang.org/x/text/unicode/norm" 14 | ) 15 | 16 | // This lib nabbed from https://github.com/extemporalgenome/slug/blob/master/slug.go 17 | 18 | var lat = []*unicode.RangeTable{unicode.Letter, unicode.Number} 19 | var nop = []*unicode.RangeTable{unicode.Mark, unicode.Sk, unicode.Lm} 20 | 21 | // Slug replaces each run of characters which are not unicode letters or 22 | // numbers with a single hyphen, except for leading or trailing runs. Letters 23 | // will be stripped of diacritical marks and lowercased. Letter or number 24 | // codepoints that do not have combining marks or a lower-cased variant will 25 | // be passed through unaltered. 26 | func Slug(s string) string { 27 | buf := make([]rune, 0, len(s)) 28 | dash := false 29 | for _, r := range norm.NFKD.String(s) { 30 | switch { 31 | // unicode 'letters' like mandarin characters pass through 32 | case unicode.IsOneOf(lat, r): 33 | buf = append(buf, unicode.ToLower(r)) 34 | dash = true 35 | case unicode.IsOneOf(nop, r): 36 | // skip 37 | case dash: 38 | buf = append(buf, '-') 39 | dash = false 40 | } 41 | } 42 | if i := len(buf) - 1; i >= 0 && buf[i] == '-' { 43 | buf = buf[:i] 44 | } 45 | return string(buf) 46 | } 47 | 48 | // SlugAscii is identical to Slug, except that runs of one or more unicode 49 | // letters or numbers that still fall outside the ASCII range will have their 50 | // UTF-8 representation hex encoded and delimited by hyphens. As with Slug, in 51 | // no case will hyphens appear at either end of the returned string. 52 | func SlugAscii(s string) string { 53 | const m = utf8.UTFMax 54 | var ( 55 | ib [m * 3]byte 56 | ob []byte 57 | buf = make([]byte, 0, len(s)) 58 | dash = false 59 | latin = true 60 | ) 61 | for _, r := range norm.NFKD.String(s) { 62 | switch { 63 | case unicode.IsOneOf(lat, r): 64 | r = unicode.ToLower(r) 65 | n := utf8.EncodeRune(ib[:m], r) 66 | if r >= 128 { 67 | if latin && dash { 68 | buf = append(buf, '-') 69 | } 70 | n = hex.Encode(ib[m:], ib[:n]) 71 | ob = ib[m : m+n] 72 | latin = false 73 | } else { 74 | if !latin { 75 | buf = append(buf, '-') 76 | } 77 | ob = ib[:n] 78 | latin = true 79 | } 80 | dash = true 81 | buf = append(buf, ob...) 82 | case unicode.IsOneOf(nop, r): 83 | // skip 84 | case dash: 85 | buf = append(buf, '-') 86 | dash = false 87 | latin = true 88 | } 89 | } 90 | if i := len(buf) - 1; i >= 0 && buf[i] == '-' { 91 | buf = buf[:i] 92 | } 93 | return string(buf) 94 | } 95 | 96 | // IsSlugAscii returns true only if SlugAscii(s) == s. 97 | func IsSlugAscii(s string) bool { 98 | dash := true 99 | for _, r := range s { 100 | switch { 101 | case r == '-': 102 | if dash { 103 | return false 104 | } 105 | dash = true 106 | case 'a' <= r && r <= 'z', '0' <= r && r <= '9': 107 | dash = false 108 | default: 109 | return false 110 | } 111 | } 112 | return !dash 113 | } 114 | -------------------------------------------------------------------------------- /tap/profileActions.go: -------------------------------------------------------------------------------- 1 | package tap 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | logger "github.com/TykTechnologies/tyk-identity-broker/log" 8 | ) 9 | 10 | var log = logger.Get() 11 | 12 | type HttpError struct { 13 | Message string 14 | Code int 15 | Error error 16 | } 17 | 18 | func AddProfile(profile Profile, AuthConfigStore AuthRegisterBackend, flush func(backend AuthRegisterBackend) error) *HttpError { 19 | dumpProfile := Profile{} 20 | keyErr := AuthConfigStore.GetKey(profile.ID, profile.OrgID, &dumpProfile) 21 | if keyErr == nil && dumpProfile.ID != "" { 22 | return &HttpError{ 23 | Message: "Object ID already exists", 24 | Code: http.StatusBadRequest, 25 | Error: keyErr, 26 | } 27 | } 28 | 29 | saveErr := AuthConfigStore.SetKey(profile.ID, profile.OrgID, &profile) 30 | if saveErr != nil { 31 | return &HttpError{ 32 | Message: "insert failed", 33 | Code: http.StatusInternalServerError, 34 | Error: saveErr, 35 | } 36 | } 37 | 38 | fErr := flush(AuthConfigStore) 39 | if fErr != nil { 40 | return &HttpError{ 41 | Message: "flush failed", 42 | Code: http.StatusBadRequest, 43 | Error: fErr, 44 | } 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func UpdateProfile(key string, profile Profile, AuthConfigStore AuthRegisterBackend, flush func(backend AuthRegisterBackend) error) *HttpError { 51 | 52 | // Shenanigans 53 | if profile.ID != key { 54 | return &HttpError{ 55 | Message: "Object ID and URI resource ID do not match", 56 | Code: http.StatusBadRequest, 57 | Error: errors.New("ID Mismatch"), 58 | } 59 | } 60 | 61 | dumpProfile := Profile{} 62 | keyErr := AuthConfigStore.GetKey(key, profile.OrgID, &dumpProfile) 63 | if keyErr != nil { 64 | return &HttpError{ 65 | Message: "Object ID does not exist, operation not permitted", 66 | Code: http.StatusNotFound, 67 | Error: keyErr, 68 | } 69 | } 70 | 71 | saveErr := AuthConfigStore.SetKey(key, profile.OrgID, &profile) 72 | if saveErr != nil { 73 | return &HttpError{ 74 | Message: "Update failed", 75 | Code: http.StatusInternalServerError, 76 | Error: saveErr, 77 | } 78 | } 79 | 80 | fErr := flush(AuthConfigStore) 81 | if fErr != nil { 82 | return &HttpError{ 83 | Message: "flush failed", 84 | Code: http.StatusBadRequest, 85 | Error: fErr, 86 | } 87 | } 88 | 89 | return nil 90 | } 91 | 92 | func DeleteProfile(key, orgID string, AuthConfigStore AuthRegisterBackend, flush func(backend AuthRegisterBackend) error) *HttpError { 93 | 94 | dumpProfile := Profile{} 95 | keyErr := AuthConfigStore.GetKey(key, orgID, &dumpProfile) 96 | if keyErr != nil { 97 | return &HttpError{ 98 | Message: "Object ID does not exist", 99 | Code: http.StatusNotFound, 100 | Error: keyErr, 101 | } 102 | } 103 | 104 | delErr := AuthConfigStore.DeleteKey(key, orgID) 105 | if delErr != nil { 106 | return &HttpError{ 107 | Message: "Delete failed", 108 | Code: http.StatusInternalServerError, 109 | Error: delErr, 110 | } 111 | } 112 | 113 | fErr := flush(AuthConfigStore) 114 | if fErr != nil { 115 | return &HttpError{ 116 | Message: "flush failed", 117 | Code: http.StatusBadRequest, 118 | Error: fErr, 119 | } 120 | } 121 | return nil 122 | } 123 | -------------------------------------------------------------------------------- /tap/profile.go: -------------------------------------------------------------------------------- 1 | /* 2 | package tap wraps a set of interfaces and object to provide a generic interface to a delegated authentication 3 | 4 | proxy 5 | */ 6 | package tap 7 | 8 | import ( 9 | "encoding/json" 10 | 11 | "github.com/TykTechnologies/storage/persistent/model" 12 | ) 13 | 14 | // I know it is not correct convention for table naming but it needs to be backward compatible :( 15 | const ProfilesCollectionName = "profilesCollection" 16 | 17 | // Profile is the configuration object for an authentication session, it 18 | // combines an Action (what to do with the identity once confirmed, this is 19 | // delegated to an IdentityHandler) with a Provider (such as Social / GPlus) 20 | type Profile struct { 21 | ID string `bson:"ID" json:"ID" gorm:"primaryKey;column:ID"` 22 | Name string `bson:"Name" json:"Name"` 23 | OrgID string `bson:"OrgID" json:"OrgID"` 24 | ActionType Action `bson:"ActionType" json:"ActionType"` 25 | MatchedPolicyID string `bson:"MatchedPolicyID" json:"MatchedPolicyID"` 26 | Type ProviderType `bson:"Type" json:"Type"` 27 | ProviderName string `bson:"ProviderName" json:"ProviderName"` 28 | CustomEmailField string `bson:"CustomEmailField" json:"CustomEmailField"` 29 | CustomUserIDField string `bson:"CustomUserIDField" json:"CustomUserIDField"` 30 | ProviderConfig interface{} `bson:"ProviderConfig" json:"ProviderConfig"` 31 | IdentityHandlerConfig map[string]interface{} `bson:"IdentityHandlerConfig" json:"IdentityHandlerConfig"` 32 | ProviderConstraints ProfileConstraint `bson:"ProviderConstraints" json:"ProviderConstraints"` 33 | ReturnURL string `bson:"ReturnURL" json:"ReturnURL"` 34 | DefaultUserGroupID string `bson:"DefaultUserGroupID" json:"DefaultUserGroupID"` 35 | CustomUserGroupField string `bson:"CustomUserGroupField" json:"CustomUserGroupField"` 36 | UserGroupMapping map[string]string `bson:"UserGroupMapping" json:"UserGroupMapping"` 37 | UserGroupSeparator string `bson:"UserGroupSeparator" json:"UserGroupSeparator"` 38 | SSOOnlyForRegisteredUsers bool `bson:"SSOOnlyForRegisteredUsers" json:"SSOOnlyForRegisteredUsers"` 39 | } 40 | 41 | func (p Profile) SetObjectID(id model.ObjectID) { 42 | } 43 | 44 | func (p Profile) TableName() string { 45 | return ProfilesCollectionName 46 | } 47 | 48 | func (p Profile) GetObjectID() model.ObjectID { 49 | return "" 50 | } 51 | 52 | // ProfileConstraint Certain providers can have constraints, this object sets out those constraints. E.g. Domain: "tyk.io" will limit 53 | // social logins to only those with a tyk.io domain name 54 | type ProfileConstraint struct { 55 | Domain string 56 | Group string 57 | } 58 | 59 | func (p Profile) UnmarshalBinary(data []byte) error { 60 | // convert data to yours, let's assume its json data 61 | return json.Unmarshal(data, p) 62 | } 63 | 64 | func (p Profile) MarshalBinary() ([]byte, error) { 65 | return json.Marshal(p) 66 | } 67 | 68 | // GetPrefix return prefix for redis 69 | func (p Profile) GetPrefix() string { 70 | return p.OrgID + "-" + p.ID 71 | } 72 | -------------------------------------------------------------------------------- /providers/tapProvider.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | 7 | "github.com/TykTechnologies/tyk-identity-broker/constants" 8 | "github.com/TykTechnologies/tyk-identity-broker/tap" 9 | identityHandlers "github.com/TykTechnologies/tyk-identity-broker/tap/identity-handlers" 10 | "github.com/TykTechnologies/tyk-identity-broker/tyk-api" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // return a provider based on the name of the provider type, add new providers here 15 | func GetTAProvider(conf tap.Profile, handler tyk.TykAPI, identityKeyStore tap.AuthRegisterBackend) (tap.TAProvider, error) { 16 | 17 | var thisProvider tap.TAProvider 18 | switch conf.ProviderName { 19 | case constants.SocialProvider: 20 | thisProvider = &Social{} 21 | case constants.ADProvider: 22 | thisProvider = &ADProvider{} 23 | case constants.ProxyProvider: 24 | thisProvider = &ProxyProvider{} 25 | case constants.SAMLProvider: 26 | thisProvider = &SAMLProvider{} 27 | default: 28 | return nil, errors.New("invalid provider name") 29 | } 30 | 31 | thisIdentityHandler := getIdentityHandler(conf.ActionType, handler, identityKeyStore) 32 | log.Debugf("Initializing Identity Handler with config: %+v", conf) 33 | thisIdentityHandler.Init(conf) 34 | log.Debug("Initializing Provider") 35 | err := thisProvider.Init(thisIdentityHandler, conf, hackProviderConf(conf.ProviderConfig)) 36 | 37 | return thisProvider, err 38 | } 39 | 40 | // Maps an identity handler from an Action type, register new Identity Handlers and methods here 41 | func getIdentityHandler(name tap.Action, handler tyk.TykAPI, identityKeyStore tap.AuthRegisterBackend) tap.IdentityHandler { 42 | var thisIdentityHandler tap.IdentityHandler 43 | 44 | switch name { 45 | case tap.GenerateOrLoginDeveloperProfile, tap.GenerateOrLoginUserProfile, tap.GenerateOAuthTokenForClient, tap.GenerateTemporaryAuthToken: 46 | thisIdentityHandler = &identityHandlers.TykIdentityHandler{ 47 | API: &handler, 48 | Store: identityKeyStore} 49 | } 50 | 51 | return thisIdentityHandler 52 | } 53 | 54 | func GetTapProfile(AuthConfigStore, identityKeyStore tap.AuthRegisterBackend, id string, tykHandler tyk.TykAPI) (tap.TAProvider, tap.Profile, *tap.HttpError) { 55 | 56 | thisProfile := tap.Profile{} 57 | log.WithField("prefix", constants.HandlerLogTag).Debug("--> Looking up profile ID: ", id) 58 | foundProfileErr := AuthConfigStore.GetKey(id, thisProfile.OrgID, &thisProfile) 59 | 60 | if foundProfileErr != nil { 61 | errorMsg := "Profile " + id + " not found" 62 | return nil, thisProfile, &tap.HttpError{ 63 | Message: errorMsg, 64 | Code: 404, 65 | Error: foundProfileErr, 66 | } 67 | } 68 | 69 | thisIdentityProvider, providerErr := GetTAProvider(thisProfile, tykHandler, identityKeyStore) 70 | if providerErr != nil { 71 | log.WithError(providerErr).Error("Getting Tap Provider") 72 | return nil, thisProfile, &tap.HttpError{ 73 | Message: "Could not initialise provider", 74 | Code: 400, 75 | Error: providerErr, 76 | } 77 | } 78 | 79 | return thisIdentityProvider, thisProfile, nil 80 | } 81 | 82 | // A hack to marshal a provider conf from map[string]interface{} into a type without type checking, ugly, but effective 83 | func hackProviderConf(conf interface{}) []byte { 84 | thisConf, err := json.Marshal(conf) 85 | if err != nil { 86 | log.WithFields(logrus.Fields{ 87 | "prefix": constants.HandlerLogTag, 88 | "error": err, 89 | }).Warning("Failure in JSON conversion") 90 | return []byte{} 91 | } 92 | return thisConf 93 | } 94 | -------------------------------------------------------------------------------- /ci/bin/dist_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo This is deprecated. Use the goreleaser based automation 4 | 5 | : ${ORGDIR:="/src/github.com/TykTechnologies"} 6 | : ${SIGNKEY:="12B5D62C28F57592D1575BD51ED14C59E37DAC20"} 7 | : ${BUILDPKGS:="1"} 8 | TYK_IB_SRC_DIR=$ORGDIR/tyk-identity-broker 9 | BUILDTOOLSDIR=$TYK_IB_SRC_DIR/build_tools 10 | 11 | echo "Set version number" 12 | : ${VERSION:=$(perl -n -e'/v(\d+).(\d+).(\d+)/'' && print "$1\.$2\.$3"' version.go)} 13 | 14 | if [ $BUILDPKGS == "1" ]; then 15 | echo Configuring gpg-agent-config to accept a passphrase 16 | mkdir ~/.gnupg && chmod 700 ~/.gnupg 17 | cat >> ~/.gnupg/gpg-agent.conf <" 65 | --url "https://tyk.io" 66 | -s dir 67 | -C $BUILD_DIR 68 | --before-install $BUILD_DIR/install/before_install.sh 69 | --after-install $BUILD_DIR/install/post_install.sh 70 | --after-remove $BUILD_DIR/install/post_remove.sh 71 | --config-files /opt/tyk-identity-broker/tib.conf 72 | ) 73 | FPMRPM=( 74 | --before-upgrade $BUILD_DIR/install/post_remove.sh 75 | --after-upgrade $BUILD_DIR/install/post_install.sh 76 | ) 77 | 78 | for arch in i386 amd64 arm64 79 | do 80 | echo "Creating $arch Tarball" 81 | cd $TYK_IB_SRC_DIR 82 | mv tyk-identity-broker_linux_${arch/i386/386} $BUILD_DIR/tyk-identity-broker 83 | cd $RELEASE_DIR 84 | tar -pczf $RELEASE_DIR/tyk-identity-broker-$arch-$VERSION.tar.gz $BUILD/ 85 | 86 | if [ $BUILDPKGS == "1" ]; then 87 | echo "Building $arch packages" 88 | fpm "${FPMCOMMON[@]}" -a $arch -t deb --deb-user tyk --deb-group tyk ./=/opt/tyk-identity-broker 89 | fpm "${FPMCOMMON[@]}" "${FPMRPM[@]}" -a $arch -t rpm --rpm-user tyk --rpm-group tyk ./=/opt/tyk-identity-broker 90 | 91 | echo "Signing $arch RPM" 92 | rpm --define "%_gpg_name Team Tyk (package signing) " \ 93 | --define "%__gpg /usr/bin/gpg" \ 94 | --addsign *.rpm || (cat /tmp/gpg-agent.log; exit 1) 95 | echo "Signing $arch DEB" 96 | for i in *.deb 97 | do 98 | dpkg-sig --sign builder -k $SIGNKEY $i || (cat /tmp/gpg-agent.log; exit 1) 99 | done 100 | fi 101 | done 102 | -------------------------------------------------------------------------------- /data_loader/mongo_loader.go: -------------------------------------------------------------------------------- 1 | package data_loader 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "time" 7 | 8 | "github.com/TykTechnologies/storage/persistent" 9 | "github.com/TykTechnologies/storage/persistent/utils" 10 | 11 | "github.com/TykTechnologies/tyk-identity-broker/tap" 12 | ) 13 | 14 | var ( 15 | mongoPrefix = "mongo" 16 | ) 17 | 18 | // MongoLoaderConf is the configuration struct for a MongoLoader 19 | type MongoLoaderConf struct { 20 | ClientOpts *persistent.ClientOpts 21 | } 22 | 23 | // MongoLoader implements DataLoader and will load TAP Profiles from a file 24 | type MongoLoader struct { 25 | config MongoLoaderConf 26 | store persistent.PersistentStorage 27 | SkipFlush bool 28 | } 29 | 30 | type ProfilesBackup struct { 31 | Timestamp int `bson:"timestamp" json:"timestamp"` 32 | Profiles []tap.Profile `bson:"profiles" json:"profiles"` 33 | } 34 | 35 | // Init initialises the mongo loader 36 | func (m *MongoLoader) Init(conf interface{}) error { 37 | mongoConfig := conf.(MongoLoaderConf) 38 | 39 | store, err := persistent.NewPersistentStorage(mongoConfig.ClientOpts) 40 | if err != nil { 41 | dataLogger.WithError(err).WithField("prefix", mongoPrefix).Error("failed to init MongoDB connection") 42 | time.Sleep(5 * time.Second) 43 | m.Init(conf) 44 | } 45 | 46 | m.store = store 47 | return err 48 | } 49 | 50 | // LoadIntoStore will load, unmarshal and copy profiles into a an AuthRegisterBackend 51 | func (m *MongoLoader) LoadIntoStore(store tap.AuthRegisterBackend) error { 52 | var profiles []tap.Profile 53 | 54 | err := m.store.Query(context.Background(), tap.Profile{}, &profiles, nil) 55 | 56 | if err != nil { 57 | dataLogger.Error("error reading profiles from mongo: " + err.Error()) 58 | return err 59 | } 60 | 61 | for _, profile := range profiles { 62 | inputErr := store.SetKey(profile.ID, profile.OrgID, profile) 63 | if inputErr != nil { 64 | dataLogger.WithField("error", inputErr).Error("Couldn't encode configuration") 65 | } 66 | } 67 | 68 | dataLogger.Info("Loaded profiles from Mongo") 69 | return nil 70 | } 71 | 72 | func handleEmptyProfilesError(err error) error { 73 | if err != nil { 74 | if !utils.IsErrNoRows(err) { 75 | dataLogger.WithError(err).Error("emptying profiles collection") 76 | return err 77 | } 78 | } 79 | return nil 80 | } 81 | 82 | // Flush creates a backup of the current loaded config 83 | func (m *MongoLoader) Flush(store tap.AuthRegisterBackend) error { 84 | //read all 85 | //save the changes in the main profile's collection, so empty and store as we don't know what was removed, updated or added 86 | updatedSet := store.GetAll("") 87 | 88 | //empty to store new changes 89 | err := m.store.Delete(context.Background(), tap.Profile{}, nil) 90 | if handledErr := handleEmptyProfilesError(err); handledErr != nil { 91 | return handledErr 92 | } 93 | 94 | for _, p := range updatedSet { 95 | profile := tap.Profile{} 96 | switch p := p.(type) { 97 | case string: 98 | // we need to make this because redis return string instead objects 99 | if err := json.Unmarshal([]byte(p), &profile); err != nil { 100 | dataLogger.WithError(err).Error("un-marshaling interface for mongo flushing") 101 | return err 102 | } 103 | default: 104 | profile = p.(tap.Profile) 105 | } 106 | 107 | err = m.store.Insert(context.Background(), profile) 108 | if err != nil { 109 | dataLogger.WithError(err).Error("error refreshing profiles records in mongo") 110 | return err 111 | } 112 | } 113 | 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /providers/FileLoader.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "io/ioutil" 5 | "strings" 6 | ) 7 | 8 | type FileLoader struct{} 9 | 10 | var FileLoaderLogTag = "CERT FILE LOADER" 11 | var FileLoaderLogger = log.WithField("prefix", FileLoaderLogTag) 12 | 13 | func (f FileLoader) GetKey(key string) (string, error) { 14 | id := strings.Trim(key, "raw-") 15 | rawCert, err := ioutil.ReadFile(id) 16 | if err != nil { 17 | FileLoaderLogger.Errorf("Could not read cert file: %v", err.Error()) 18 | } 19 | return string(rawCert), err 20 | } 21 | 22 | func (f FileLoader) SetKey(string, string, int64) error { 23 | panic("implement me") 24 | } 25 | 26 | func (f FileLoader) GetKeys(string) []string { 27 | panic("implement me") 28 | } 29 | 30 | func (f FileLoader) DeleteKey(string) bool { 31 | panic("implement me") 32 | } 33 | 34 | func (f FileLoader) DeleteScanMatch(string) bool { 35 | panic("implement me") 36 | } 37 | 38 | func (f FileLoader) GetListRange(string, int64, int64) ([]string, error) { 39 | panic("implement me") 40 | } 41 | 42 | func (f FileLoader) RemoveFromList(string, string) error { 43 | panic("implement me") 44 | } 45 | 46 | func (f FileLoader) AppendToSet(string, string) { 47 | panic("implement me") 48 | } 49 | 50 | func (f FileLoader) Exists(string) (bool, error) { 51 | panic("implement me") 52 | } 53 | 54 | func (f FileLoader) AddToSet(string, string) { 55 | panic("implement me") 56 | } 57 | 58 | func (f FileLoader) AddToSortedSet(string, string, float64) { 59 | panic("implement me") 60 | } 61 | 62 | func (f FileLoader) Connect() bool { 63 | panic("implement me") 64 | } 65 | 66 | func (f FileLoader) Decrement(string) { 67 | panic("implement me") 68 | } 69 | 70 | func (f FileLoader) DeleteAllKeys() bool { 71 | panic("implement me") 72 | } 73 | 74 | func (f FileLoader) DeleteKeys([]string) bool { 75 | panic("implement me") 76 | } 77 | 78 | func (f FileLoader) DeleteRawKey(string) bool { 79 | panic("implement me") 80 | } 81 | 82 | func (f FileLoader) GetAndDeleteSet(string) []interface{} { 83 | panic("implement me") 84 | } 85 | 86 | func (f FileLoader) GetExp(string) (int64, error) { 87 | panic("implement me") 88 | } 89 | 90 | func (f FileLoader) GetKeyPrefix() string { 91 | panic("implement me") 92 | } 93 | 94 | func (f FileLoader) GetKeysAndValues() map[string]string { 95 | panic("implement me") 96 | } 97 | 98 | func (f FileLoader) GetKeysAndValuesWithFilter(string) map[string]string { 99 | panic("implement me") 100 | } 101 | 102 | func (f FileLoader) GetMultiKey([]string) ([]string, error) { 103 | panic("implement me") 104 | } 105 | 106 | func (f FileLoader) GetRawKey(string) (string, error) { 107 | panic("implement me") 108 | } 109 | 110 | func (f FileLoader) GetRollingWindow(key string, per int64, pipeline bool) (int, []interface{}) { 111 | panic("implement me") 112 | } 113 | 114 | func (f FileLoader) GetSet(string) (map[string]string, error) { 115 | panic("implement me") 116 | } 117 | 118 | func (f FileLoader) GetSortedSetRange(string, string, string) ([]string, []float64, error) { 119 | panic("implement me") 120 | } 121 | 122 | func (f FileLoader) IncrememntWithExpire(string, int64) int64 { 123 | panic("implement me") 124 | } 125 | 126 | func (f FileLoader) RemoveFromSet(string, string) { 127 | panic("implement me") 128 | } 129 | 130 | func (f FileLoader) RemoveSortedSetRange(string, string, string) error { 131 | panic("implement me") 132 | } 133 | 134 | func (f FileLoader) SetExp(string, int64) error { 135 | panic("implement me") 136 | } 137 | 138 | func (f FileLoader) SetRawKey(string, string, int64) error { 139 | panic("implement me") 140 | } 141 | 142 | func (f FileLoader) SetRollingWindow(key string, per int64, val string, pipeline bool) (int, []interface{}) { 143 | panic("implement me") 144 | } 145 | 146 | func (f FileLoader) DeleteRawKeys([]string) bool { 147 | panic("implement me") 148 | } 149 | -------------------------------------------------------------------------------- /internal/jwe/JWE_test.go: -------------------------------------------------------------------------------- 1 | package jwe 2 | 3 | import ( 4 | "crypto/rsa" 5 | "testing" 6 | 7 | "github.com/markbates/goth/providers/openidConnect" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | // Test case for Handler.Decrypt 13 | func TestHandler_Decrypt(t *testing.T) { 14 | // Generate a mock private key 15 | mockCert, err := GenerateMockPrivateKey() 16 | assert.NoError(t, err) 17 | 18 | // Create a valid JWE token for testing 19 | jweString, err := CreateJWE([]byte("test token"), mockCert.PrivateKey.(*rsa.PrivateKey).Public().(*rsa.PublicKey)) 20 | assert.NoError(t, err) 21 | 22 | tests := []struct { 23 | name string 24 | handler *Handler 25 | token string 26 | expected string 27 | expectError bool 28 | errorMessage string 29 | }{ 30 | { 31 | name: "Disabled Handler", 32 | handler: &Handler{ 33 | Enabled: false, 34 | }, 35 | token: jweString, 36 | expected: jweString, 37 | expectError: false, 38 | }, 39 | { 40 | name: "Key Not Loaded", 41 | handler: &Handler{ 42 | Enabled: true, 43 | Key: nil, 44 | }, 45 | token: jweString, 46 | expected: "", 47 | expectError: true, 48 | errorMessage: "JWE Private Key not loaded", 49 | }, 50 | { 51 | name: "Successful Decryption", 52 | handler: &Handler{ 53 | Enabled: true, 54 | Key: mockCert, 55 | }, 56 | token: jweString, 57 | expected: "test token", 58 | expectError: false, 59 | }, 60 | { 61 | name: "Invalid Token", 62 | handler: &Handler{ 63 | Enabled: true, 64 | Key: mockCert, 65 | }, 66 | token: "invalid-token", 67 | expected: "", 68 | expectError: true, 69 | errorMessage: "error parsing JWE", 70 | }, 71 | } 72 | 73 | for _, tt := range tests { 74 | t.Run(tt.name, func(t *testing.T) { 75 | decrypted, err := tt.handler.Decrypt(tt.token) 76 | 77 | if tt.expectError { 78 | assert.Error(t, err) 79 | assert.Contains(t, err.Error(), tt.errorMessage) 80 | } else { 81 | assert.NoError(t, err) 82 | assert.Equal(t, tt.expected, decrypted) 83 | } 84 | }) 85 | } 86 | } 87 | 88 | func TestDecryptIDToken(t *testing.T) { 89 | mockCert, err := GenerateMockPrivateKey() 90 | assert.NoError(t, err) 91 | 92 | // Create a valid JWE token for testing 93 | jweString, err := CreateJWE([]byte("test token"), mockCert.PrivateKey.(*rsa.PrivateKey).Public().(*rsa.PublicKey)) 94 | assert.NoError(t, err) 95 | 96 | // Setup a valid JWE handler 97 | jweHandler := &Handler{ 98 | Enabled: true, 99 | Key: mockCert, 100 | } 101 | 102 | tests := []struct { 103 | name string 104 | jwtSession *openidConnect.Session 105 | expectError bool 106 | expectedToken string 107 | jweHandler *Handler 108 | }{ 109 | { 110 | name: "Successful Decryption", 111 | jwtSession: &openidConnect.Session{ 112 | IDToken: jweString, 113 | }, 114 | expectError: false, 115 | expectedToken: "test token", 116 | jweHandler: jweHandler, 117 | }, 118 | { 119 | name: "Invalid Token", 120 | jwtSession: &openidConnect.Session{ 121 | IDToken: "invalid-token", 122 | }, 123 | expectError: true, 124 | jweHandler: jweHandler, 125 | }, 126 | { 127 | name: "Failed Decryption due Key not loaded", 128 | jwtSession: &openidConnect.Session{ 129 | IDToken: jweString, 130 | }, 131 | expectError: true, 132 | expectedToken: "test token", 133 | jweHandler: &Handler{ 134 | Enabled: true, 135 | }, 136 | }, 137 | } 138 | 139 | for _, tt := range tests { 140 | t.Run(tt.name, func(t *testing.T) { 141 | err := DecryptIDToken(tt.jweHandler, tt.jwtSession) 142 | 143 | if tt.expectError { 144 | assert.Error(t, err) 145 | } else { 146 | assert.NoError(t, err) 147 | assert.Equal(t, tt.expectedToken, tt.jwtSession.IDToken) 148 | } 149 | }) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /ci/install/inits/sysv/init.d/tyk-identity-broker: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Init script for tyk-identity-broker 3 | # Maintained by 4 | # Generated by pleaserun. 5 | # Implemented based on LSB Core 3.1: 6 | # * Sections: 20.2, 20.3 7 | # 8 | ### BEGIN INIT INFO 9 | # Provides: tyk-identity-broker 10 | # Required-Start: $remote_fs $syslog 11 | # Required-Stop: $remote_fs $syslog 12 | # Default-Start: 2 3 4 5 13 | # Default-Stop: 0 1 6 14 | # Short-Description: 15 | # Description: Tyk Identity Broker 16 | ### END INIT INFO 17 | 18 | PATH=/sbin:/usr/sbin:/bin:/usr/bin 19 | export PATH 20 | 21 | name=tyk-identity-broker 22 | program=/opt/tyk-identity-broker/tyk-identity-broker 23 | args='-c /opt/tyk-identity-broker/tib.conf' 24 | pidfile="/var/run/$name.pid" 25 | 26 | [ -r /etc/default/$name ] && . /etc/default/$name 27 | [ -r /etc/sysconfig/$name ] && . /etc/sysconfig/$name 28 | 29 | trace() { 30 | logger -t "/etc/init.d/tyk-identity-broker" "$@" 31 | } 32 | 33 | emit() { 34 | trace "$@" 35 | echo "$@" 36 | } 37 | 38 | start() { 39 | # Setup any environmental stuff beforehand 40 | 41 | 42 | # Run the program! 43 | 44 | chroot --userspec "$user":"$group" "$chroot" sh -c " 45 | 46 | cd \"$chdir\" 47 | exec \"$program\" $args 48 | " >> /var/log/tyk-identity-broker.stdout 2>> /var/log/tyk-identity-broker.stderr & 49 | 50 | # Generate the pidfile from here. If we instead made the forked process 51 | # generate it there will be a race condition between the pidfile writing 52 | # and a process possibly asking for status. 53 | echo $! > $pidfile 54 | 55 | emit "$name started" 56 | return 0 57 | } 58 | 59 | stop() { 60 | # Try a few times to kill TERM the program 61 | if status ; then 62 | pid=$(cat "$pidfile") 63 | trace "Killing $name (pid $pid) with SIGTERM" 64 | kill -TERM $pid 65 | # Wait for it to exit. 66 | for i in 1 2 3 4 5 ; do 67 | trace "Waiting $name (pid $pid) to die..." 68 | status || break 69 | sleep 1 70 | done 71 | if status ; then 72 | emit "$name stop failed; still running." 73 | else 74 | emit "$name stopped." 75 | fi 76 | fi 77 | } 78 | 79 | status() { 80 | if [ -f "$pidfile" ] ; then 81 | pid=$(cat "$pidfile") 82 | if ps -p $pid > /dev/null 2> /dev/null ; then 83 | # process by this pid is running. 84 | # It may not be our pid, but that's what you get with just pidfiles. 85 | # TODO(sissel): Check if this process seems to be the same as the one we 86 | # expect. It'd be nice to use flock here, but flock uses fork, not exec, 87 | # so it makes it quite awkward to use in this case. 88 | return 0 89 | else 90 | return 2 # program is dead but pid file exists 91 | fi 92 | else 93 | return 3 # program is not running 94 | fi 95 | } 96 | 97 | force_stop() { 98 | if status ; then 99 | stop 100 | status && kill -KILL $(cat "$pidfile") 101 | fi 102 | } 103 | 104 | 105 | case "$1" in 106 | force-start|start|stop|force-stop|restart) 107 | trace "Attempting '$1' on tyk-identity-broker" 108 | ;; 109 | esac 110 | 111 | case "$1" in 112 | force-start) 113 | PRESTART=no 114 | exec "$0" start 115 | ;; 116 | start) 117 | status 118 | code=$? 119 | if [ $code -eq 0 ]; then 120 | emit "$name is already running" 121 | exit $code 122 | else 123 | start 124 | exit $? 125 | fi 126 | ;; 127 | stop) stop ;; 128 | force-stop) force_stop ;; 129 | status) 130 | status 131 | code=$? 132 | if [ $code -eq 0 ] ; then 133 | emit "$name is running" 134 | else 135 | emit "$name is not running" 136 | fi 137 | exit $code 138 | ;; 139 | restart) 140 | 141 | stop && start 142 | ;; 143 | *) 144 | echo "Usage: $SCRIPTNAME {start|force-start|stop|force-start|force-stop|status|restart}" >&2 145 | exit 3 146 | ;; 147 | esac 148 | 149 | exit $? 150 | -------------------------------------------------------------------------------- /ci/install/post_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | 4 | # Generated by: gromit policy 5 | 6 | # If "True" the install directory ownership will be changed to "tyk:tyk" 7 | change_ownership="True" 8 | 9 | # Step 1, decide if we should use systemd or init/upstart 10 | use_systemctl="True" 11 | systemd_version=0 12 | if ! command -V systemctl >/dev/null 2>&1; then 13 | use_systemctl="False" 14 | else 15 | systemd_version=$(systemctl --version | head -1 | sed 's/systemd //g') 16 | fi 17 | 18 | cleanup() { 19 | # After installing, remove files that were not needed on this platform / system 20 | if [ "${use_systemctl}" = "True" ]; then 21 | rm -f /lib/systemd/system/tyk-identity-broker.service 22 | else 23 | rm -f /etc/init.d/tyk-identity-broker 24 | fi 25 | } 26 | 27 | restoreServices() { 28 | if [ "${use_systemctl}" = "True" ]; then 29 | if [ ! -f /lib/systemd/system/tyk-identity-broker.service ]; then 30 | cp /opt/tyk-identity-broker/install/inits/systemd/system/tyk-identity-broker.service /lib/systemd/system/tyk-identity-broker.service 31 | fi 32 | else 33 | if [ ! -f /etc/init.d/tyk-identity-broker ]; then 34 | cp /opt/tyk-identity-broker/install/inits/sysv/init.d/tyk-identity-broker /etc/init.d/tyk-identity-broker 35 | fi 36 | fi 37 | } 38 | 39 | setupOwnership() { 40 | printf "\033[32m Post Install of the install directory ownership and permissions\033[0m\n" 41 | [ "${change_ownership}" = "True" ] && chown -R tyk:tyk /opt/tyk-identity-broker 42 | # Config file should never be world-readable 43 | chmod 660 /opt/tyk-identity-broker/tib.conf 44 | } 45 | 46 | cleanInstall() { 47 | printf "\033[32m Post Install of an clean install\033[0m\n" 48 | # Step 3 (clean install), enable the service in the proper way for this platform 49 | if [ "${use_systemctl}" = "False" ]; then 50 | if command -V chkconfig >/dev/null 2>&1; then 51 | chkconfig --add tyk-identity-broker 52 | chkconfig tyk-identity-broker on 53 | fi 54 | if command -V update-rc.d >/dev/null 2>&1; then 55 | update-rc.d tyk-identity-broker defaults 56 | fi 57 | 58 | service tyk-identity-broker restart ||: 59 | else 60 | printf "\033[32m Reload the service unit from disk\033[0m\n" 61 | systemctl daemon-reload ||: 62 | printf "\033[32m Unmask the service\033[0m\n" 63 | systemctl unmask tyk-identity-broker ||: 64 | printf "\033[32m Set the preset flag for the service unit\033[0m\n" 65 | systemctl preset tyk-identity-broker ||: 66 | printf "\033[32m Set the enabled flag for the service unit\033[0m\n" 67 | systemctl enable tyk-identity-broker ||: 68 | systemctl restart tyk-identity-broker ||: 69 | fi 70 | } 71 | 72 | upgrade() { 73 | printf "\033[32m Post Install of an upgrade\033[0m\n" 74 | if [ "${use_systemctl}" = "False" ]; then 75 | service tyk-identity-broker restart 76 | else 77 | systemctl daemon-reload ||: 78 | systemctl restart tyk-identity-broker ||: 79 | fi 80 | } 81 | 82 | # Step 2, check if this is a clean install or an upgrade 83 | action="$1" 84 | if [ "$1" = "configure" ] && [ -z "$2" ]; then 85 | # Alpine linux does not pass args, and deb passes $1=configure 86 | action="install" 87 | elif [ "$1" = "configure" ] && [ -n "$2" ]; then 88 | # deb passes $1=configure $2= 89 | action="upgrade" 90 | fi 91 | 92 | case "$action" in 93 | "1" | "install") 94 | setupOwnership 95 | cleanInstall 96 | ;; 97 | "2" | "upgrade") 98 | printf "\033[32m Post Install of an upgrade\033[0m\n" 99 | setupOwnership 100 | restoreServices 101 | upgrade 102 | ;; 103 | *) 104 | # $1 == version being installed 105 | printf "\033[32m Alpine\033[0m" 106 | setupOwnership 107 | cleanInstall 108 | ;; 109 | esac 110 | 111 | # From https://www.debian.org/doc/debian-policy/ap-flowcharts.html and 112 | # https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/ it appears that cleanup is not 113 | # needed to support systemd and sysv 114 | 115 | #cleanup 116 | -------------------------------------------------------------------------------- /providers/proxy_test.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/matryer/is" 11 | 12 | "github.com/TykTechnologies/tyk-identity-broker/tap" 13 | identityHandlers "github.com/TykTechnologies/tyk-identity-broker/tap/identity-handlers" 14 | ) 15 | 16 | const proxyConfig_CODE = ` 17 | { 18 | "TargetHost" : "http://lonelycode.com/doesnotexist", 19 | "OKCode" : 200, 20 | "OKResponse" : "", 21 | "OKRegex" : "", 22 | "ResponseIsJson": false, 23 | "AccessTokenField": "", 24 | "UsernameField": "", 25 | "ExrtactUserNameFromBasicAuthHeader": false 26 | } 27 | ` 28 | 29 | const proxyConfig_BODY = ` 30 | { 31 | "TargetHost" : "http://lonelycode.com", 32 | "OKCode" : 0, 33 | "OKResponse" : "This wont match", 34 | "OKRegex" : "", 35 | "ResponseIsJson": false, 36 | "AccessTokenField": "", 37 | "UsernameField": "", 38 | "ExrtactUserNameFromBasicAuthHeader": false 39 | } 40 | ` 41 | 42 | const proxyConfig_REGEX = ` 43 | { 44 | "TargetHost" : "https://lonelycode.com", 45 | "OKCode" : 0, 46 | "OKResponse" : "", 47 | "OKRegex" : "digital hippie", 48 | "ResponseIsJson": false, 49 | "AccessTokenField": "", 50 | "UsernameField": "", 51 | "ExrtactUserNameFromBasicAuthHeader": false 52 | } 53 | ` 54 | 55 | const BODYFAILURE_STR = "Authentication Failed" 56 | 57 | func getProfile(t *testing.T, profileConfig string) tap.Profile { 58 | t.Helper() 59 | 60 | is := is.New(t) 61 | 62 | provConf := ProxyHandlerConfig{} 63 | is.NoErr(json.Unmarshal([]byte(profileConfig), &provConf)) 64 | 65 | thisProfile := tap.Profile{ 66 | ID: "1", 67 | OrgID: "12345", 68 | ActionType: "GenerateTemporaryAuthToken", 69 | Type: "passthrough", 70 | ProviderName: "ProxyProvider", 71 | ProviderConfig: provConf, 72 | IdentityHandlerConfig: make(map[string]interface{}), 73 | } 74 | 75 | return thisProfile 76 | } 77 | 78 | func TestProxyProvider_BadCode(t *testing.T) { 79 | is := is.New(t) 80 | 81 | thisConf := proxyConfig_CODE 82 | thisProfile := getProfile(t, thisConf) 83 | thisProvider := ProxyProvider{} 84 | 85 | is.NoErr(thisProvider.Init(identityHandlers.DummyIdentityHandler{}, thisProfile, []byte(thisConf))) 86 | 87 | recorder := httptest.NewRecorder() 88 | req, err := http.NewRequest("GET", "/", nil) 89 | if err != nil { 90 | t.Fatal(err) 91 | } 92 | 93 | thisProvider.Handle(recorder, req, nil, thisProfile) 94 | thisBody, err := ioutil.ReadAll(recorder.Body) 95 | is.NoErr(err) 96 | 97 | if recorder.Code != 401 { 98 | t.Fatalf("Expected 401 response code, got '%d'", recorder.Code) 99 | } 100 | 101 | if string(thisBody) != BODYFAILURE_STR { 102 | t.Fatalf("Body string '%s' is incorrect", thisBody) 103 | } 104 | } 105 | 106 | func TestProxyProvider_BadBody(t *testing.T) { 107 | is := is.New(t) 108 | 109 | thisConf := proxyConfig_BODY 110 | thisProfile := getProfile(t, thisConf) 111 | thisProvider := ProxyProvider{} 112 | 113 | is.NoErr(thisProvider.Init(identityHandlers.DummyIdentityHandler{}, thisProfile, []byte(thisConf))) 114 | 115 | recorder := httptest.NewRecorder() 116 | req, err := http.NewRequest("GET", "/", nil) 117 | if err != nil { 118 | t.Fatal(err) 119 | } 120 | 121 | thisProvider.Handle(recorder, req, nil, thisProfile) 122 | thisBody, err := ioutil.ReadAll(recorder.Body) 123 | is.NoErr(err) 124 | 125 | if recorder.Code != 401 { 126 | t.Fatalf("Expected 401 response code, got '%d'", recorder.Code) 127 | } 128 | 129 | if string(thisBody) != BODYFAILURE_STR { 130 | t.Fatalf("Body string '%s' is incorrect", thisBody) 131 | } 132 | } 133 | 134 | func TestProxyProvider_GoodRegex(t *testing.T) { 135 | is := is.New(t) 136 | 137 | thisConf := proxyConfig_REGEX 138 | thisProfile := getProfile(t, thisConf) 139 | thisProvider := ProxyProvider{} 140 | 141 | is.NoErr(thisProvider.Init(identityHandlers.DummyIdentityHandler{}, thisProfile, []byte(thisConf))) 142 | 143 | recorder := httptest.NewRecorder() 144 | req, err := http.NewRequest("GET", "/", nil) 145 | if err != nil { 146 | t.Fatal(err) 147 | } 148 | 149 | thisProvider.Handle(recorder, req, nil, thisProfile) 150 | 151 | if recorder.Code != 200 { 152 | t.Fatalf("Expected 200 response code, got '%v'", recorder.Code) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /tothic/tothic_test.go: -------------------------------------------------------------------------------- 1 | package tothic 2 | 3 | import ( 4 | "crypto/rsa" 5 | "net/http" 6 | "net/url" 7 | "os" 8 | "reflect" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/TykTechnologies/tyk-identity-broker/internal/jwe" 13 | "github.com/markbates/goth" 14 | "github.com/markbates/goth/providers/openidConnect" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func TestKeyFromEnv(t *testing.T) { 19 | expected := "SECRET" 20 | 21 | t.Run("with new variable", func(t *testing.T) { 22 | _ = os.Setenv("TYK_IB_SESSION_SECRET", expected) 23 | _ = os.Setenv("SESSION_SECRET", "") 24 | assertDeepEqual(t, expected, KeyFromEnv()) 25 | }) 26 | 27 | t.Run("with deprecated", func(t *testing.T) { 28 | _ = os.Setenv("TYK_IB_SESSION_SECRET", "") 29 | _ = os.Setenv("SESSION_SECRET", expected) 30 | assertDeepEqual(t, expected, KeyFromEnv()) 31 | }) 32 | 33 | t.Run("with both", func(t *testing.T) { 34 | _ = os.Setenv("SESSION_SECRET", "SOME_OTHER_SECRET") 35 | _ = os.Setenv("TYK_IB_SESSION_SECRET", expected) 36 | assertDeepEqual(t, expected, KeyFromEnv()) 37 | }) 38 | } 39 | 40 | func TestProcessJWTSession_Success(t *testing.T) { 41 | 42 | mockCert, err := jwe.GenerateMockPrivateKey() 43 | assert.NoError(t, err) 44 | 45 | IDTokenContents := "test-id-token" 46 | 47 | // Create a valid JWE token for testing 48 | jweString, err := jwe.CreateJWE([]byte(IDTokenContents), mockCert.PrivateKey.(*rsa.PrivateKey).Public().(*rsa.PublicKey)) 49 | assert.NoError(t, err) 50 | 51 | tcs := []struct { 52 | name string 53 | sess goth.Session 54 | jweHandler jwe.Handler 55 | expectedSess goth.Session 56 | errExpected bool 57 | }{ 58 | { 59 | name: "no id token encryption", 60 | sess: &openidConnect.Session{ 61 | IDToken: "anything", 62 | }, 63 | expectedSess: &openidConnect.Session{ 64 | IDToken: "anything", 65 | }, 66 | jweHandler: jwe.Handler{Enabled: false}, 67 | errExpected: false, 68 | }, 69 | { 70 | name: "failed decryption, no key present", 71 | sess: &openidConnect.Session{ 72 | IDToken: "any-encrypted-val", 73 | }, 74 | expectedSess: &openidConnect.Session{ 75 | IDToken: "any-encrypted-val", 76 | }, 77 | jweHandler: jwe.Handler{Enabled: true}, 78 | errExpected: true, 79 | }, 80 | { 81 | name: "successful decryption", 82 | sess: &openidConnect.Session{ 83 | IDToken: jweString, 84 | }, 85 | jweHandler: jwe.Handler{ 86 | Enabled: true, 87 | Key: mockCert, 88 | }, 89 | expectedSess: &openidConnect.Session{ 90 | IDToken: IDTokenContents, 91 | }, 92 | errExpected: false, 93 | }, 94 | } 95 | 96 | for _, tc := range tcs { 97 | t.Run(tc.name, func(t *testing.T) { 98 | // Call the function 99 | sessResult, err := prepareJWTSession(tc.sess, &tc.jweHandler) 100 | // Assert results 101 | didErr := err != nil 102 | assert.Equal(t, tc.errExpected, didErr) 103 | if !tc.errExpected { 104 | assert.Equal(t, tc.expectedSess, sessResult) 105 | } 106 | }) 107 | } 108 | 109 | } 110 | 111 | func assertDeepEqual(t *testing.T, expected interface{}, actual interface{}) { 112 | if !reflect.DeepEqual(expected, actual) { 113 | t.Errorf("Expected %v, actual %v", expected, actual) 114 | } 115 | } 116 | 117 | func TestGetState(t *testing.T) { 118 | req, _ := http.NewRequest(http.MethodGet, "http://localhost", nil) 119 | assert.Equal(t, "state", GetState(req)) 120 | 121 | req, _ = http.NewRequest(http.MethodGet, "http://localhost?state=FooBar", nil) 122 | assert.Equal(t, "FooBar", GetState(req)) 123 | 124 | req, _ = http.NewRequest(http.MethodPost, "http://localhost", nil) 125 | assert.Equal(t, "state", GetState(req)) 126 | 127 | req, _ = http.NewRequest(http.MethodPost, "http://localhost?state=FooBar", nil) 128 | assert.Equal(t, "FooBar", GetState(req)) 129 | 130 | data := url.Values{} 131 | data.Add("state", "BarBaz") 132 | 133 | requestBody := data.Encode() 134 | 135 | req, _ = http.NewRequest(http.MethodPost, "http://localhost", strings.NewReader(requestBody)) 136 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 137 | 138 | assert.Equal(t, "BarBaz", GetState(req)) 139 | 140 | req, _ = http.NewRequest(http.MethodPost, "http://localhost?state=FooBar", strings.NewReader(requestBody)) 141 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 142 | 143 | assert.Equal(t, "FooBar", GetState(req)) 144 | } 145 | -------------------------------------------------------------------------------- /ci/goreleaser/goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Generated by: gromit policy 2 | 3 | # Check the documentation at http://goreleaser.com 4 | # This project needs CGO_ENABLED=1 and the cross-compiler toolchains for 5 | # - arm64 6 | # - amd64 7 | version: 2 8 | builds: 9 | - id: std 10 | ldflags: 11 | - -X github.com/TykTechnologies/tyk-identity-broker/main.Version={{.Version}} 12 | - -X github.com/TykTechnologies/tyk-identity-broker/main.Commit={{.FullCommit}} 13 | - -X github.com/TykTechnologies/tyk-identity-broker/main.BuildDate={{.Date}} 14 | - -X github.com/TykTechnologies/tyk-identity-broker/main.BuiltBy=goreleaser 15 | goos: 16 | - linux 17 | goarch: 18 | - amd64 19 | - arm64 20 | binary: tyk-identity-broker 21 | - id: fips 22 | ldflags: 23 | - -X github.com/TykTechnologies/tyk-identity-broker/main.Version={{.Version}} 24 | - -X github.com/TykTechnologies/tyk-identity-broker/main.Commit={{.FullCommit}} 25 | - -X github.com/TykTechnologies/tyk-identity-broker/main.BuildDate={{.Date}} 26 | - -X github.com/TykTechnologies/tyk-identity-broker/main.BuiltBy=goreleaser 27 | goos: 28 | - linux 29 | goarch: 30 | - amd64 31 | binary: tyk-identity-broker 32 | nfpms: 33 | - id: std 34 | vendor: "Tyk Technologies Ltd" 35 | homepage: "https://tyk.io" 36 | maintainer: "Tyk " 37 | description: Tyk Authentication Proxy for third-party login 38 | package_name: tyk-identity-broker 39 | file_name_template: "{{ .ConventionalFileName }}" 40 | builds: 41 | - std 42 | formats: 43 | - deb 44 | - rpm 45 | contents: 46 | - src: "README.md" 47 | dst: "/opt/share/docs/tyk-identity-broker/README.md" 48 | - src: "ci/install/*" 49 | dst: "/opt/tyk-identity-broker/install" 50 | - src: ci/install/inits/systemd/system/tyk-identity-broker.service 51 | dst: /lib/systemd/system/tyk-identity-broker.service 52 | - src: ci/install/inits/sysv/init.d/tyk-identity-broker 53 | dst: /etc/init.d/tyk-identity-broker 54 | - src: "LICENSE.md" 55 | dst: "/opt/share/docs/tyk-identity-broker/LICENSE.md" 56 | - src: tib_sample.conf 57 | dst: /opt/tyk-identity-broker/tib.conf 58 | type: "config|noreplace" 59 | scripts: 60 | preinstall: "ci/install/before_install.sh" 61 | postinstall: "ci/install/post_install.sh" 62 | postremove: "ci/install/post_remove.sh" 63 | bindir: "/opt/tyk-identity-broker" 64 | rpm: 65 | scripts: 66 | posttrans: ci/install/post_trans.sh 67 | signature: 68 | key_file: tyk.io.signing.key 69 | deb: 70 | signature: 71 | key_file: tyk.io.signing.key 72 | type: origin 73 | - id: fips 74 | vendor: "Tyk Technologies Ltd" 75 | homepage: "https://tyk.io" 76 | maintainer: "Tyk " 77 | description: Tyk Authentication Proxy for third-party login 78 | package_name: tyk-identity-broker-fips 79 | file_name_template: "{{ .ConventionalFileName }}" 80 | builds: 81 | - fips 82 | formats: 83 | - deb 84 | - rpm 85 | contents: 86 | - src: "README.md" 87 | dst: "/opt/share/docs/tyk-identity-broker/README.md" 88 | - src: "ci/install/*" 89 | dst: "/opt/tyk-identity-broker/install" 90 | - src: ci/install/inits/systemd/system/tyk-identity-broker.service 91 | dst: /lib/systemd/system/tyk-identity-broker.service 92 | - src: ci/install/inits/sysv/init.d/tyk-identity-broker 93 | dst: /etc/init.d/tyk-identity-broker 94 | - src: "LICENSE.md" 95 | dst: "/opt/share/docs/tyk-identity-broker/LICENSE.md" 96 | - src: tib_sample.conf 97 | dst: /opt/tyk-identity-broker/tib.conf 98 | type: "config|noreplace" 99 | scripts: 100 | preinstall: "ci/install/before_install.sh" 101 | postinstall: "ci/install/post_install.sh" 102 | postremove: "ci/install/post_remove.sh" 103 | bindir: "/opt/tyk-identity-broker" 104 | rpm: 105 | scripts: 106 | posttrans: ci/install/post_trans.sh 107 | signature: 108 | key_file: tyk.io.signing.key 109 | deb: 110 | signature: 111 | key_file: tyk.io.signing.key 112 | type: origin 113 | publishers: 114 | - name: tyk-identity-broker-unstable 115 | env: 116 | - PACKAGECLOUD_TOKEN={{ .Env.PACKAGECLOUD_TOKEN }} 117 | cmd: packagecloud publish --debvers "{{ .Env.DEBVERS }}" --rpmvers "{{ .Env.RPMVERS }}" tyk/tyk-identity-broker-unstable {{ .ArtifactPath }} 118 | # This disables archives 119 | archives: 120 | - format: binary 121 | allow_different_binary_count: true 122 | checksum: 123 | disable: true 124 | release: 125 | disable: true 126 | github: 127 | owner: TykTechnologies 128 | name: tyk-identity-broker 129 | prerelease: auto 130 | draft: true 131 | name_template: "{{.ProjectName}}-v{{.Version}}" 132 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | 10 | tykerror "github.com/TykTechnologies/tyk-identity-broker/error" 11 | 12 | "github.com/TykTechnologies/tyk-identity-broker/tap" 13 | "github.com/gorilla/mux" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | var APILogTag string = "API" 18 | 19 | type APIOKMessage struct { 20 | Status string 21 | ID string 22 | Data interface{} 23 | } 24 | 25 | func HandleAPIOK(data interface{}, id string, code int, w http.ResponseWriter, r *http.Request) { 26 | okObj := APIOKMessage{ 27 | Status: "ok", 28 | ID: id, 29 | Data: data, 30 | } 31 | 32 | responseMsg, err := json.Marshal(&okObj) 33 | 34 | if err != nil { 35 | log.WithFields(logrus.Fields{ 36 | "prefix": APILogTag, 37 | "error": err, 38 | }).Error("[OK Handler] Couldn't marshal message stats") 39 | fmt.Fprintf(w, "System Error") 40 | return 41 | } 42 | 43 | w.Header().Set("Content-Type", "application/json") 44 | w.WriteHeader(code) 45 | fmt.Fprintf(w, string(responseMsg)) 46 | } 47 | 48 | func HandleAPIError(tag string, errorMsg string, rawErr error, code int, w http.ResponseWriter, r *http.Request) { 49 | log.WithFields(logrus.Fields{ 50 | "prefix": tag, 51 | "error": errorMsg, 52 | }).Error(rawErr) 53 | 54 | errorObj := tykerror.APIErrorMessage{Status: "error", Error: errorMsg} 55 | responseMsg, err := json.Marshal(&errorObj) 56 | 57 | if err != nil { 58 | log.WithFields(logrus.Fields{ 59 | "prefix": tag, 60 | "error": err, 61 | }).Error("[Error Handler] Couldn't marshal error stats") 62 | fmt.Fprintf(w, "System Error") 63 | return 64 | } 65 | 66 | w.Header().Set("Content-Type", "application/json") 67 | w.WriteHeader(code) 68 | fmt.Fprintf(w, string(responseMsg)) 69 | } 70 | 71 | // ------ Middleware methods ------- 72 | func IsAuthenticated(h http.Handler) http.Handler { 73 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 74 | if r.Header.Get("Authorization") != config.Secret { 75 | HandleAPIError(APILogTag, "Authorization failed", errors.New("Header mismatch"), 401, w, r) 76 | return 77 | } 78 | 79 | h.ServeHTTP(w, r) 80 | }) 81 | } 82 | 83 | // ------ End Middleware methods ------- 84 | 85 | func HandleGetProfileList(w http.ResponseWriter, r *http.Request) { 86 | profiles := AuthConfigStore.GetAll("") 87 | 88 | HandleAPIOK(profiles, "", 200, w, r) 89 | } 90 | 91 | func HandleGetProfile(w http.ResponseWriter, r *http.Request) { 92 | key := mux.Vars(r)["id"] 93 | thisProfile := tap.Profile{} 94 | 95 | keyErr := AuthConfigStore.GetKey(key, thisProfile.OrgID, &thisProfile) 96 | if keyErr != nil { 97 | HandleAPIError(APILogTag, "Profile not found", keyErr, 404, w, r) 98 | return 99 | } 100 | 101 | HandleAPIOK(thisProfile, key, 200, w, r) 102 | } 103 | 104 | func HandleAddProfile(w http.ResponseWriter, r *http.Request) { 105 | key := mux.Vars(r)["id"] 106 | 107 | profileData, err := ioutil.ReadAll(r.Body) 108 | if err != nil { 109 | HandleAPIError(APILogTag, "Invalid request data", err, 400, w, r) 110 | return 111 | } 112 | 113 | thisProfile := tap.Profile{} 114 | decodeErr := json.Unmarshal(profileData, &thisProfile) 115 | if decodeErr != nil { 116 | HandleAPIError(APILogTag, "Failed to decode body data", decodeErr, 400, w, r) 117 | return 118 | } 119 | 120 | if thisProfile.ID != key { 121 | HandleAPIError(APILogTag, "Object ID and URI resource ID do not match", errors.New("ID Mismatch"), 400, w, r) 122 | return 123 | } 124 | 125 | httpErr := tap.AddProfile(thisProfile, AuthConfigStore, GlobalDataLoader.Flush) 126 | if httpErr != nil { 127 | HandleAPIError(APILogTag, httpErr.Message, httpErr.Error, httpErr.Code, w, r) 128 | return 129 | } 130 | 131 | HandleAPIOK(thisProfile, key, 201, w, r) 132 | } 133 | 134 | func HandleUpdateProfile(w http.ResponseWriter, r *http.Request) { 135 | key := mux.Vars(r)["id"] 136 | 137 | profileData, err := ioutil.ReadAll(r.Body) 138 | if err != nil { 139 | HandleAPIError(APILogTag, "Invalid request data", err, 400, w, r) 140 | return 141 | } 142 | 143 | thisProfile := tap.Profile{} 144 | decodeErr := json.Unmarshal(profileData, &thisProfile) 145 | if decodeErr != nil { 146 | HandleAPIError(APILogTag, "Failed to decode body data", decodeErr, 400, w, r) 147 | return 148 | } 149 | 150 | updateErr := tap.UpdateProfile(key, thisProfile, AuthConfigStore, GlobalDataLoader.Flush) 151 | if updateErr != nil { 152 | HandleAPIError(APILogTag, updateErr.Message, updateErr.Error, updateErr.Code, w, r) 153 | return 154 | } 155 | 156 | HandleAPIOK(thisProfile, key, 200, w, r) 157 | } 158 | 159 | func HandleDeleteProfile(w http.ResponseWriter, r *http.Request) { 160 | key := mux.Vars(r)["id"] 161 | err := tap.DeleteProfile(key, "", AuthConfigStore, GlobalDataLoader.Flush) 162 | if err != nil { 163 | HandleAPIError(APILogTag, err.Message, err.Error, err.Code, w, r) 164 | return 165 | } 166 | 167 | data := make(map[string]string) 168 | HandleAPIOK(data, key, 200, w, r) 169 | } 170 | -------------------------------------------------------------------------------- /configuration/config.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | 9 | "github.com/TykTechnologies/storage/persistent" 10 | 11 | "github.com/kelseyhightower/envconfig" 12 | "github.com/sirupsen/logrus" 13 | 14 | logger "github.com/TykTechnologies/tyk-identity-broker/log" 15 | "github.com/TykTechnologies/tyk-identity-broker/tothic" 16 | tyk "github.com/TykTechnologies/tyk-identity-broker/tyk-api" 17 | ) 18 | 19 | var failCount int 20 | var log = logger.Get() 21 | var mainLoggerTag = "CONFIG" 22 | var mainLogger = log.WithField("prefix", mainLoggerTag) 23 | 24 | const ( 25 | MONGO = "mongo" 26 | FILE = "file" 27 | ) 28 | 29 | type IdentityBackendSettings struct { 30 | Database int 31 | Username string 32 | Password string 33 | Host string 34 | Port int 35 | Timeout int 36 | MaxIdle int 37 | MaxActive int 38 | UseSSL bool 39 | SSLInsecureSkipVerify bool 40 | CAFile string 41 | CertFile string 42 | KeyFile string 43 | MaxVersion string 44 | MinVersion string 45 | EnableCluster bool 46 | Addrs []string 47 | Hosts map[string]string // Deprecated: Use Addrs instead. 48 | MasterName string 49 | SentinelPassword string 50 | } 51 | 52 | type MongoConf struct { 53 | DbName string `json:"db_name" mapstructure:"db_name"` 54 | MongoURL string `json:"mongo_url" mapstructure:"mongo_url"` 55 | MongoUseSSL bool `json:"mongo_use_ssl" mapstructure:"mongo_use_ssl"` 56 | MongoSSLInsecureSkipVerify bool `json:"mongo_ssl_insecure_skip_verify" mapstructure:"mongo_ssl_insecure_skip_verify"` 57 | MaxInsertBatchSizeBytes int `json:"max_insert_batch_size_bytes" mapstructure:"max_insert_batch_size_bytes"` 58 | MaxDocumentSizeBytes int `json:"max_document_size_bytes" mapstructure:"max_document_size_bytes"` 59 | CollectionCapMaxSizeBytes int `json:"collection_cap_max_size_bytes" mapstructure:"collection_cap_max_size_bytes"` 60 | CollectionCapEnable bool `json:"collection_cap_enable" mapstructure:"collection_cap_enable"` 61 | SessionConsistency string `json:"session_consistency" mapstructure:"session_consistency"` 62 | Driver string `json:"driver" mapstructure:"driver"` 63 | DirectConnection bool `json:"direct_connection" mapstructure:"direct_connection"` 64 | } 65 | 66 | type TLS struct { 67 | } 68 | 69 | // Storage object to configure the storage where the profiles lives in 70 | // it can be extended to work with other loaders. As file Loader is the default 71 | // then we dont read the file path from here 72 | type Storage struct { 73 | StorageType string `json:"storage_type" mapstructure:"storage_type"` 74 | MongoConf *MongoConf `json:"mongo" mapstructure:"mongo"` 75 | } 76 | 77 | // FileLoaderConf is the configuration struct for a FileLoader, takes a filename as main init 78 | type FileLoaderConf struct { 79 | FileName string 80 | ProfileDir string 81 | } 82 | 83 | type Backend struct { 84 | ProfileBackendSettings interface{} 85 | IdentityBackendSettings IdentityBackendSettings 86 | } 87 | 88 | // Configuration holds all configuration settings for TAP 89 | type Configuration struct { 90 | Secret string 91 | Port int 92 | ProfileDir string 93 | BackEnd Backend 94 | TykAPISettings tyk.TykAPI 95 | HttpServerOptions struct { 96 | UseSSL bool 97 | CertFile string 98 | KeyFile string 99 | SSLInsecureSkipVerify bool 100 | } 101 | SSLInsecureSkipVerify bool 102 | Storage *Storage 103 | } 104 | 105 | // LoadConfig will load the config from a file 106 | func LoadConfig(filePath string, conf *Configuration) { 107 | log = logger.Get() 108 | mainLogger = &logrus.Entry{Logger: log} 109 | mainLogger = mainLogger.Logger.WithField("prefix", mainLoggerTag) 110 | 111 | configuration, err := ioutil.ReadFile(filePath) 112 | if err != nil { 113 | mainLogger.Error("Couldn't load configuration file: ", err) 114 | failCount += 1 115 | if failCount < 3 { 116 | LoadConfig(filePath, conf) 117 | } else { 118 | mainLogger.Fatal("Could not open configuration, giving up.") 119 | } 120 | } else { 121 | jsErr := json.Unmarshal(configuration, conf) 122 | if jsErr != nil { 123 | mainLogger.Error("Couldn't unmarshal configuration: ", jsErr) 124 | } 125 | } 126 | 127 | shouldOmit, omitEnvExist := os.LookupEnv(tothic.EnvPrefix + "_OMITCONFIGFILE") 128 | if omitEnvExist && strings.ToLower(shouldOmit) == "true" { 129 | *conf = Configuration{} 130 | } 131 | 132 | if err = envconfig.Process(tothic.EnvPrefix, conf); err != nil { 133 | mainLogger.Errorf("Failed to process config env vars: %v", err) 134 | } 135 | 136 | mainLogger.Debugf("\nConfig Loaded: %+v \n", conf) 137 | mainLogger.Debugf("\n Storage conf: %+v \n", conf.Storage) 138 | mainLogger.Debug("Settings Struct: ", conf.TykAPISettings) 139 | } 140 | 141 | // GetMongoDriver returns a valid mongo driver to use, it receives the 142 | // driver set in config, and check its validity 143 | // otherwise default to mongo-go 144 | func GetMongoDriver(driverFromConf string) string { 145 | if driverFromConf != persistent.Mgo && driverFromConf != persistent.OfficialMongo { 146 | return persistent.OfficialMongo 147 | } 148 | return driverFromConf 149 | } 150 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "flag" 6 | "net" 7 | "net/http" 8 | "strconv" 9 | 10 | "github.com/TykTechnologies/tyk-identity-broker/backends" 11 | "github.com/TykTechnologies/tyk-identity-broker/configuration" 12 | "github.com/TykTechnologies/tyk-identity-broker/data_loader" 13 | "github.com/TykTechnologies/tyk-identity-broker/initializer" 14 | 15 | errors "github.com/TykTechnologies/tyk-identity-broker/error" 16 | logger "github.com/TykTechnologies/tyk-identity-broker/log" 17 | "github.com/TykTechnologies/tyk-identity-broker/tap" 18 | "github.com/TykTechnologies/tyk-identity-broker/tothic" 19 | "github.com/TykTechnologies/tyk-identity-broker/tyk-api" 20 | "github.com/gorilla/mux" 21 | ) 22 | 23 | // AuthConfigStore Is the back end we are storing our configuration files to 24 | var AuthConfigStore tap.AuthRegisterBackend 25 | 26 | // IdentityKeyStore keeps a record of identities tied to tokens (if needed) 27 | var IdentityKeyStore tap.AuthRegisterBackend 28 | 29 | // config is the system-wide configuration 30 | var config configuration.Configuration 31 | 32 | // TykAPIHandler is a global API handler for Tyk, wraps the tyk APi in Go functions 33 | var TykAPIHandler tyk.TykAPI 34 | 35 | var GlobalDataLoader data_loader.DataLoader 36 | 37 | var log = logger.Get() 38 | var mainLogger = log.WithField("prefix", "MAIN") 39 | var ProfileFilename, confFile string 40 | 41 | func init() { 42 | mainLogger.Info("Tyk Identity Broker ", Version) 43 | mainLogger.Info("Copyright Tyk Technologies Ltd 2020") 44 | 45 | flag.StringVar(&confFile, "conf", "tib.conf", "Path to the config file") 46 | flag.StringVar(&confFile, "c", "tib.conf", "Path to the config file") 47 | flag.StringVar(&ProfileFilename, "p", "./profiles.json", "Path to the profiles file") 48 | flag.Parse() 49 | 50 | configuration.LoadConfig(confFile, &config) 51 | AuthConfigStore, IdentityKeyStore = initializer.InitBackend(config.BackEnd.ProfileBackendSettings, config.BackEnd.IdentityBackendSettings) 52 | 53 | configStore := &backends.RedisBackend{KeyPrefix: "tib-provider-config-"} 54 | configStore.Init(config.BackEnd.IdentityBackendSettings) 55 | initializer.SetConfigHandler(configStore) 56 | 57 | TykAPIHandler = config.TykAPISettings 58 | 59 | // In OIDC there are calls to the https://{IDP-DOMAIN}/.well-know/openid-configuration and other endpoints 60 | // We set the http client's Transport to do InsecureSkipVerify to avoid error in case the certificate 61 | // was signed by unknown authority, trusting the user to set up his profile with the correct .well-know URL. 62 | http.DefaultClient.Transport = &http.Transport{ 63 | Proxy: http.ProxyFromEnvironment, 64 | TLSClientConfig: &tls.Config{ 65 | InsecureSkipVerify: config.SSLInsecureSkipVerify, 66 | }, 67 | } 68 | var err error 69 | GlobalDataLoader, err = data_loader.CreateDataLoader(config, ProfileFilename) 70 | if err != nil { 71 | return 72 | } 73 | err = GlobalDataLoader.LoadIntoStore(AuthConfigStore) 74 | if err != nil { 75 | mainLogger.Errorf("loading into store %v", err) 76 | return 77 | } 78 | 79 | tothic.TothErrorHandler = errors.HandleError 80 | tothic.SetupSessionStore() 81 | } 82 | 83 | func main() { 84 | p := mux.NewRouter() 85 | p.Handle("/auth/{id}/{provider}/callback", http.HandlerFunc(HandleAuthCallback)) 86 | p.Handle("/auth/{id}/{provider}", http.HandlerFunc(HandleAuth)) 87 | p.Handle("/auth/{id}/saml/metadata", http.HandlerFunc(HandleMetadata)) 88 | 89 | p.Handle("/api/profiles/{id}", IsAuthenticated(http.HandlerFunc(HandleGetProfile))).Methods("GET") 90 | p.Handle("/api/profiles/{id}", IsAuthenticated(http.HandlerFunc(HandleAddProfile))).Methods("POST") 91 | p.Handle("/api/profiles/{id}", IsAuthenticated(http.HandlerFunc(HandleUpdateProfile))).Methods("PUT") 92 | p.Handle("/api/profiles/{id}", IsAuthenticated(http.HandlerFunc(HandleDeleteProfile))).Methods("DELETE") 93 | 94 | p.Handle("/api/profiles", IsAuthenticated(http.HandlerFunc(HandleGetProfileList))).Methods("GET") 95 | 96 | p.Handle("/health", http.HandlerFunc(HandleHealthCheck)).Methods("GET") 97 | 98 | listenPort := 3010 99 | if config.Port != 0 { 100 | listenPort = config.Port 101 | } 102 | 103 | var tibServer net.Listener 104 | if config.HttpServerOptions.UseSSL { 105 | mainLogger.Info("--> Using SSL (https) for TIB") 106 | cert, err := tls.LoadX509KeyPair(config.HttpServerOptions.CertFile, config.HttpServerOptions.KeyFile) 107 | 108 | if err != nil { 109 | mainLogger.WithError(err).Error("loading cert file") 110 | return 111 | } 112 | 113 | cfg := tls.Config{ 114 | Certificates: []tls.Certificate{cert}, 115 | InsecureSkipVerify: config.HttpServerOptions.SSLInsecureSkipVerify, 116 | } 117 | tibServer = createListener(listenPort, &cfg) 118 | } else { 119 | mainLogger.Info("--> Standard listener (http) for TIB") 120 | tibServer = createListener(listenPort, nil) 121 | } 122 | _ = http.Serve(tibServer, p) 123 | 124 | } 125 | 126 | func createListener(port int, tlsConfig *tls.Config) (listener net.Listener) { 127 | var err error 128 | addr := ":" + strconv.Itoa(port) 129 | 130 | if tlsConfig != nil { 131 | listener, err = tls.Listen("tcp", addr, tlsConfig) 132 | 133 | // to consume Dash api, then we skip as well the verification in the client side 134 | tr := &http.Transport{TLSClientConfig: tlsConfig} 135 | c := &http.Client{Transport: tr} 136 | tyk.SetHttpClient(c) 137 | } else { 138 | listener, err = net.Listen("tcp", addr) 139 | } 140 | if err != nil { 141 | log.Panic("Server creation failed! ", err) 142 | } 143 | 144 | return 145 | } 146 | -------------------------------------------------------------------------------- /.github/workflows/ci-tests.yml: -------------------------------------------------------------------------------- 1 | name: Go Test Workflow 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - release-** 8 | pull_request: 9 | branches: 10 | - master 11 | - release-** 12 | env: 13 | TYK_IB_STORAGE_STORAGETYPE: file 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | golangci-lint: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout Tyk Identity Broker 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 2 26 | - name: Fetch base branch 27 | if: ${{ github.event_name == 'pull_request' }} 28 | run: git fetch origin ${{ github.base_ref }} 29 | - name: golangci-lint 30 | if: ${{ github.event_name == 'pull_request' }} 31 | uses: golangci/golangci-lint-action@v3 32 | with: 33 | version: latest 34 | args: --out-format=checkstyle:golanglint.xml --timeout=600s --max-issues-per-linter=0 --max-same-issues=0 --new-from-rev=origin/${{ github.base_ref }} 35 | - uses: actions/upload-artifact@v4 36 | with: 37 | name: golangcilint 38 | retention-days: 1 39 | path: | 40 | golanglint.xml 41 | 42 | ci-test: 43 | name: "${{ matrix.databases }}" 44 | runs-on: ubuntu-latest 45 | strategy: 46 | fail-fast: false 47 | matrix: 48 | databases: 49 | - mongo-mgo 50 | - mongo-official 51 | - file 52 | redis-version: [5] 53 | mongodb-version: [4.2] 54 | steps: 55 | - name: Checkout Code 56 | uses: actions/checkout@v4 57 | with: 58 | fetch-depth: 2 59 | 60 | - name: Setup Go 61 | uses: actions/setup-go@v2 62 | with: 63 | go-version: "^1.21" 64 | 65 | - name: Install Dependencies and basic hygiene test 66 | id: hygiene 67 | run: | 68 | go install golang.org/x/tools/cmd/goimports@latest 69 | 70 | - name: Start Redis 71 | uses: supercharge/redis-github-action@1.2.0 72 | with: 73 | redis-version: ${{ matrix.redis-version }} 74 | 75 | - name: Start MongoDB 76 | uses: supercharge/mongodb-github-action@1.2.0 77 | with: 78 | mongodb-version: "${{ matrix.mongodb-version }}" 79 | 80 | - name: Run tests 81 | run: | 82 | ./bin/ci-tests.sh ${{ matrix.databases }} 83 | - uses: actions/upload-artifact@v4 84 | with: 85 | name: coverage 86 | retention-days: 1 87 | path: | 88 | *cov 89 | sonar-cloud-analysis: 90 | runs-on: ubuntu-latest 91 | needs: [ci-test, golangci-lint] 92 | steps: 93 | - name: Checkout TIB 94 | uses: actions/checkout@v4 95 | with: 96 | fetch-depth: 2 97 | - name: Fetch base branch 98 | if: ${{ github.event_name == 'pull_request' }} 99 | run: git fetch origin ${{ github.base_ref }} 100 | - name: Setup Golang 101 | uses: actions/setup-go@v3 102 | with: 103 | go-version: 1.22.6 104 | - name: Download coverage artifacts 105 | uses: actions/download-artifact@v4 106 | with: 107 | name: coverage 108 | - name: Download golangcilint artifacts 109 | uses: actions/download-artifact@v4 110 | with: 111 | name: golangcilint 112 | - name: Check reports existence 113 | id: check_files 114 | uses: andstor/file-existence-action@v1 115 | with: 116 | files: "*.cov, golanglint.xml" 117 | - name: Install Dependencies 118 | run: > 119 | go install github.com/wadey/gocovmerge@latest 120 | 121 | - name: merge reports 122 | run: | 123 | ./bin/merge-cov.sh 124 | - name: SonarCloud Scan 125 | uses: sonarsource/sonarcloud-github-action@master 126 | with: 127 | args: > 128 | -Dsonar.organization=tyktechnologies 129 | -Dsonar.projectKey=TykTechnologies_tyk-identity-broker 130 | -Dsonar.sources=. 131 | -Dsonar.exclusions=ci/** 132 | -Dsonar.coverage.exclusions=**/*_test.go,**/mocks/*.go 133 | -Dsonar.test.inclusions=**/*_test.go 134 | -Dsonar.tests=. 135 | -Dsonar.go.coverage.reportPaths=*.cov 136 | -Dsonar.go.golangci-lint.reportPaths=golanglint.xml 137 | env: 138 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 139 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 140 | aggregator-ci-test: 141 | name: Unit Tests & Linting 142 | runs-on: ubuntu-latest 143 | if: ${{ always() && github.event_name == 'pull_request' }} 144 | needs: [ ci-test ] 145 | steps: 146 | - name: Aggregate results 147 | run: | 148 | failed=() 149 | # Get the needs context as JSON once 150 | needs_json='${{ toJSON(needs) }}' 151 | 152 | # Loop through all jobs in the needs context 153 | for job in $(echo "$needs_json" | jq -r 'keys[]'); do 154 | job_result=$(echo "$needs_json" | jq -r --arg job "$job" '.[$job].result') 155 | 156 | if [[ "$job_result" != "success" ]]; then 157 | failed+=("$job") 158 | fi 159 | done 160 | 161 | if (( ${#failed[@]} )); then 162 | # Join the failed job names with commas 163 | failed_jobs=$(IFS=", "; echo "${failed[*]}") 164 | echo "❌ Failed jobs ----- : $failed_jobs" 165 | exit 1 166 | fi 167 | 168 | echo "✅ All required jobs succeeded" 169 | 170 | -------------------------------------------------------------------------------- /configuration/config_test.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/TykTechnologies/storage/persistent" 10 | 11 | "github.com/matryer/is" 12 | ) 13 | 14 | func TestOverrideConfigWithEnvVars(t *testing.T) { 15 | is := is.New(t) 16 | 17 | secret := "SECRET" 18 | port := 1234 19 | profileDir := "PROFILEDIR" 20 | 21 | is.NoErr(os.Setenv("TYK_IB_SECRET", secret)) 22 | is.NoErr(os.Setenv("TYK_IB_PORT", strconv.Itoa(port))) 23 | is.NoErr(os.Setenv("TYK_IB_PROFILEDIR", profileDir)) 24 | is.NoErr(os.Setenv("TYK_IB_SSLINSECURESKIPVERIFY", "true")) 25 | 26 | // Backend 27 | maxIdle := 1020 28 | maxActive := 2020 29 | database := 1 30 | password := "PASSWORD" 31 | hosts := map[string]string{ 32 | "dummyhost1": "1234", 33 | "dummyhost2": "5678", 34 | } 35 | 36 | //redis tls config 37 | redisCaFile := "test-ca-file" 38 | redisCertFile := "test-cert-file" 39 | redisKeyFile := "test-key-file" 40 | redisMaxVersion := "1.2" 41 | redisMinVersion := "1.0" 42 | 43 | var hostsStr string 44 | for key, value := range hosts { 45 | if hostsStr != "" { 46 | hostsStr += "," 47 | } 48 | hostsStr += fmt.Sprintf("%s:%s", key, value) 49 | } 50 | is.NoErr(os.Setenv("TYK_IB_BACKEND_IDENTITYBACKENDSETTINGS_MAXIDLE", strconv.Itoa(maxIdle))) 51 | is.NoErr(os.Setenv("TYK_IB_BACKEND_IDENTITYBACKENDSETTINGS_MAXACTIVE", strconv.Itoa(maxActive))) 52 | is.NoErr(os.Setenv("TYK_IB_BACKEND_IDENTITYBACKENDSETTINGS_DATABASE", strconv.Itoa(database))) 53 | is.NoErr(os.Setenv("TYK_IB_BACKEND_IDENTITYBACKENDSETTINGS_PASSWORD", password)) 54 | is.NoErr(os.Setenv("TYK_IB_BACKEND_IDENTITYBACKENDSETTINGS_ENABLECLUSTER", "true")) 55 | is.NoErr(os.Setenv("TYK_IB_BACKEND_IDENTITYBACKENDSETTINGS_HOSTS", hostsStr)) 56 | is.NoErr(os.Setenv("TYK_IB_BACKEND_IDENTITYBACKENDSETTINGS_CAFILE", redisCaFile)) 57 | is.NoErr(os.Setenv("TYK_IB_BACKEND_IDENTITYBACKENDSETTINGS_CERTFILE", redisCertFile)) 58 | is.NoErr(os.Setenv("TYK_IB_BACKEND_IDENTITYBACKENDSETTINGS_KEYFILE", redisKeyFile)) 59 | is.NoErr(os.Setenv("TYK_IB_BACKEND_IDENTITYBACKENDSETTINGS_MAXVERSION", redisMaxVersion)) 60 | is.NoErr(os.Setenv("TYK_IB_BACKEND_IDENTITYBACKENDSETTINGS_MINVERSION", redisMinVersion)) 61 | 62 | // TykAPISettings.GatewayConfig 63 | gwEndpoint := "http://dummyhost" 64 | gwPort := "7890" 65 | gwAdminSecret := "76543" 66 | is.NoErr(os.Setenv("TYK_IB_TYKAPISETTINGS_GATEWAYCONFIG_ENDPOINT", gwEndpoint)) 67 | is.NoErr(os.Setenv("TYK_IB_TYKAPISETTINGS_GATEWAYCONFIG_PORT", gwPort)) 68 | is.NoErr(os.Setenv("TYK_IB_TYKAPISETTINGS_GATEWAYCONFIG_ADMINSECRET", gwAdminSecret)) 69 | 70 | // TykAPISettings.DashboardConfig 71 | dbEndpoint := "http://dummyhost2" 72 | dbPort := "9876" 73 | dbAdminSecret := "87654" 74 | is.NoErr(os.Setenv("TYK_IB_TYKAPISETTINGS_DASHBOARDCONFIG_ENDPOINT", dbEndpoint)) 75 | is.NoErr(os.Setenv("TYK_IB_TYKAPISETTINGS_DASHBOARDCONFIG_PORT", dbPort)) 76 | is.NoErr(os.Setenv("TYK_IB_TYKAPISETTINGS_DASHBOARDCONFIG_ADMINSECRET", dbAdminSecret)) 77 | 78 | // HttpServerOptions 79 | certFile := "./certs/server.pem" 80 | keyFile := "./certs/key.pem" 81 | is.NoErr(os.Setenv("TYK_IB_HTTPSERVEROPTIONS_USESSL", "true")) 82 | is.NoErr(os.Setenv("TYK_IB_HTTPSERVEROPTIONS_CERTFILE", certFile)) 83 | is.NoErr(os.Setenv("TYK_IB_HTTPSERVEROPTIONS_KEYFILE", keyFile)) 84 | 85 | // Assertions 86 | var conf Configuration 87 | LoadConfig("testdata/tib_test.conf", &conf) 88 | 89 | is.Equal(secret, conf.Secret) 90 | is.Equal(port, conf.Port) 91 | is.Equal(profileDir, conf.ProfileDir) 92 | is.Equal(true, conf.SSLInsecureSkipVerify) 93 | 94 | is.Equal(maxIdle, conf.BackEnd.IdentityBackendSettings.MaxIdle) 95 | is.Equal(maxActive, conf.BackEnd.IdentityBackendSettings.MaxActive) 96 | is.Equal(database, conf.BackEnd.IdentityBackendSettings.Database) 97 | is.Equal(password, conf.BackEnd.IdentityBackendSettings.Password) 98 | is.Equal(true, conf.BackEnd.IdentityBackendSettings.EnableCluster) 99 | is.Equal(hosts, conf.BackEnd.IdentityBackendSettings.Hosts) 100 | is.Equal(redisMaxVersion, conf.BackEnd.IdentityBackendSettings.MaxVersion) 101 | is.Equal(redisMinVersion, conf.BackEnd.IdentityBackendSettings.MinVersion) 102 | is.Equal(redisKeyFile, conf.BackEnd.IdentityBackendSettings.KeyFile) 103 | is.Equal(redisCertFile, conf.BackEnd.IdentityBackendSettings.CertFile) 104 | is.Equal(redisCaFile, conf.BackEnd.IdentityBackendSettings.CAFile) 105 | 106 | is.Equal(gwEndpoint, conf.TykAPISettings.GatewayConfig.Endpoint) 107 | is.Equal(gwPort, conf.TykAPISettings.GatewayConfig.Port) 108 | is.Equal(gwAdminSecret, conf.TykAPISettings.GatewayConfig.AdminSecret) 109 | is.Equal(dbEndpoint, conf.TykAPISettings.DashboardConfig.Endpoint) 110 | is.Equal(dbPort, conf.TykAPISettings.DashboardConfig.Port) 111 | is.Equal(dbAdminSecret, conf.TykAPISettings.DashboardConfig.AdminSecret) 112 | 113 | is.Equal(true, conf.HttpServerOptions.UseSSL) 114 | is.Equal(certFile, conf.HttpServerOptions.CertFile) 115 | is.Equal(keyFile, conf.HttpServerOptions.KeyFile) 116 | } 117 | 118 | func TestGetMongoDriver(t *testing.T) { 119 | tests := []struct { 120 | name string 121 | driverFromConf string 122 | expected string 123 | }{ 124 | { 125 | name: "valid persistent.Mgo", 126 | driverFromConf: persistent.Mgo, 127 | expected: persistent.Mgo, 128 | }, 129 | { 130 | name: "valid persistent.OfficialMongo", 131 | driverFromConf: persistent.OfficialMongo, 132 | expected: persistent.OfficialMongo, 133 | }, 134 | { 135 | name: "invalid driverFromConf", 136 | driverFromConf: "invalidDriver", 137 | expected: persistent.OfficialMongo, 138 | }, 139 | } 140 | 141 | for _, tt := range tests { 142 | t.Run(tt.name, func(t *testing.T) { 143 | if got := GetMongoDriver(tt.driverFromConf); got != tt.expected { 144 | t.Errorf("GetMongoDriver() = %v, want %v", got, tt.expected) 145 | } 146 | }) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /providers/proxy.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | b64 "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/http/httptest" 10 | "net/http/httputil" 11 | "net/url" 12 | "regexp" 13 | "strings" 14 | "sync" 15 | 16 | "github.com/Jeffail/gabs" 17 | "github.com/markbates/goth" 18 | "github.com/sirupsen/logrus" 19 | 20 | logger "github.com/TykTechnologies/tyk-identity-broker/log" 21 | "github.com/TykTechnologies/tyk-identity-broker/tap" 22 | ) 23 | 24 | var onceReloadProxyLogger sync.Once 25 | var proxyLogTag = "PROXY PROVIDER" 26 | var proxyLogger = log.WithField("prefix", proxyLogTag) 27 | 28 | type ProxyHandlerConfig struct { 29 | TargetHost string 30 | OKCode int 31 | OKResponse string 32 | OKRegex string 33 | ResponseIsJson bool 34 | AccessTokenField string 35 | UsernameField string 36 | ExrtactUserNameFromBasicAuthHeader bool 37 | } 38 | 39 | type ProxyProvider struct { 40 | handler tap.IdentityHandler 41 | config ProxyHandlerConfig 42 | profile tap.Profile 43 | } 44 | 45 | func (p *ProxyProvider) Init(handler tap.IdentityHandler, profile tap.Profile, config []byte) error { 46 | 47 | //if a logger was set, then lets reload it to inherit those configs 48 | onceReloadProxyLogger.Do(func() { 49 | log = logger.Get() 50 | proxyLogger = &logrus.Entry{Logger: log} 51 | proxyLogger = proxyLogger.Logger.WithField("prefix", proxyLogTag) 52 | }) 53 | 54 | p.handler = handler 55 | p.profile = profile 56 | 57 | unmarshallErr := json.Unmarshal(config, &p.config) 58 | if unmarshallErr != nil { 59 | return unmarshallErr 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func (p *ProxyProvider) Name() string { 66 | return "ProxyProvider" 67 | } 68 | 69 | func (p *ProxyProvider) ProviderType() tap.ProviderType { 70 | return tap.PASSTHROUGH_PROVIDER 71 | } 72 | 73 | func (p *ProxyProvider) UseCallback() bool { 74 | return false 75 | } 76 | 77 | func (p *ProxyProvider) respondFailure(rw http.ResponseWriter, r *http.Request) { 78 | rw.WriteHeader(401) 79 | fmt.Fprintf(rw, "Authentication Failed") 80 | } 81 | 82 | func (p *ProxyProvider) Handle(rw http.ResponseWriter, r *http.Request, pathParams map[string]string, 83 | profile tap.Profile) { 84 | // copy the request to a target 85 | 86 | target, tErr := url.Parse(p.config.TargetHost) 87 | if tErr != nil { 88 | proxyLogger.WithFields(logrus.Fields{ 89 | "error": tErr, 90 | }).Error("Failed to parse target URL") 91 | p.respondFailure(rw, r) 92 | return 93 | } 94 | thisProxy := httputil.NewSingleHostReverseProxy(target) 95 | // intercept the response 96 | recorder := httptest.NewRecorder() 97 | r.URL.Path = "" 98 | r.Host = target.Host 99 | thisProxy.ServeHTTP(recorder, r) 100 | 101 | if recorder.Code >= 400 { 102 | proxyLogger.Error("Code was: ", recorder.Code) 103 | p.respondFailure(rw, r) 104 | return 105 | } 106 | // check against passing signal 107 | if p.config.OKCode != 0 { 108 | if recorder.Code != p.config.OKCode { 109 | proxyLogger.Error("Code was: ", recorder.Code, " expected: ", p.config.OKCode) 110 | p.respondFailure(rw, r) 111 | return 112 | } 113 | } 114 | 115 | thisBody, err := ioutil.ReadAll(recorder.Body) 116 | if p.config.OKResponse != "" { 117 | sEnc := b64.StdEncoding.EncodeToString(thisBody) 118 | if err != nil { 119 | proxyLogger.Error("Could not read body.") 120 | p.respondFailure(rw, r) 121 | return 122 | } 123 | 124 | if sEnc != p.config.OKResponse { 125 | shortStr := sEnc 126 | if len(sEnc) > 21 { 127 | shortStr = sEnc[:20] + "..." 128 | } 129 | proxyLogger.Error("Response was: '", shortStr, "' expected: '", p.config.OKResponse, "'") 130 | p.respondFailure(rw, r) 131 | return 132 | } 133 | } 134 | 135 | if p.config.OKRegex != "" { 136 | thisRegex, rErr := regexp.Compile(p.config.OKRegex) 137 | if rErr != nil { 138 | proxyLogger.WithField("error", err).Error("Regex failure") 139 | p.respondFailure(rw, r) 140 | return 141 | } 142 | 143 | found := thisRegex.MatchString(string(thisBody)) 144 | 145 | if !found { 146 | proxyLogger.Error("Regex not found") 147 | p.respondFailure(rw, r) 148 | return 149 | } 150 | } 151 | 152 | uName := RandStringRunes(12) 153 | if p.config.ExrtactUserNameFromBasicAuthHeader { 154 | thisU, _ := ExtractBAUsernameAndPasswordFromRequest(r) 155 | if thisU != "" { 156 | uName = thisU 157 | } 158 | } 159 | 160 | AccessToken := "" 161 | if p.config.ResponseIsJson { 162 | parsed, pErr := gabs.ParseJSON(thisBody) 163 | if pErr != nil { 164 | proxyLogger.Warning("Parsing for access token field failed") 165 | } else { 166 | if p.config.AccessTokenField != "" { 167 | tok, fT := parsed.Path(p.config.AccessTokenField).Data().(string) 168 | if fT { 169 | AccessToken = tok 170 | } 171 | } 172 | if p.config.UsernameField != "" { 173 | thisU, fU := parsed.Path(p.config.UsernameField).Data().(string) 174 | if fU { 175 | uName = thisU 176 | } 177 | } 178 | } 179 | } 180 | 181 | thisUser := goth.User{ 182 | UserID: uName, 183 | Provider: p.Name(), 184 | AccessToken: AccessToken, 185 | } 186 | 187 | // If it is already email 188 | if strings.Contains(uName, "@") { 189 | thisUser.Email = uName 190 | } else { 191 | thisUser.Email = uName + "@soSession.com" 192 | } 193 | 194 | proxyLogger.Info("Username: ", thisUser.UserID) 195 | proxyLogger.Info("Access token: ", thisUser.AccessToken) 196 | 197 | // Complete the identity action 198 | p.handler.CompleteIdentityAction(rw, r, thisUser, p.profile) 199 | } 200 | 201 | func (p *ProxyProvider) HandleCallback(http.ResponseWriter, *http.Request, func(tag string, errorMsg string, 202 | rawErr error, code int, w http.ResponseWriter, r *http.Request), tap.Profile) { 203 | } 204 | 205 | func (s *ProxyProvider) HandleMetadata(http.ResponseWriter, *http.Request) { 206 | proxyLogger.Warning("metadata not implemented for provider") 207 | } 208 | -------------------------------------------------------------------------------- /backends/redis.go: -------------------------------------------------------------------------------- 1 | package backends 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "strings" 7 | "sync/atomic" 8 | 9 | "github.com/TykTechnologies/tyk-identity-broker/configuration" 10 | 11 | "github.com/TykTechnologies/storage/temporal/connector" 12 | temporal "github.com/TykTechnologies/storage/temporal/keyvalue" 13 | "github.com/TykTechnologies/storage/temporal/model" 14 | 15 | "github.com/TykTechnologies/tyk-identity-broker/log" 16 | 17 | "github.com/sirupsen/logrus" 18 | ) 19 | 20 | var redisLoggerTag = "TIB REDIS STORE" 21 | var redisLogger = logger.WithField("prefix", redisLoggerTag) 22 | 23 | var singlePool atomic.Value 24 | var singleCachePool atomic.Value 25 | var redisUp atomic.Value 26 | var ctx = context.Background() 27 | 28 | type RedisConfig configuration.IdentityBackendSettings 29 | 30 | type RedisBackend struct { 31 | kv temporal.KeyValue 32 | config *RedisConfig 33 | HashKeys bool 34 | KeyPrefix string 35 | } 36 | 37 | type KeyError struct{} 38 | 39 | func (e KeyError) Error() string { 40 | return "Key not found" 41 | } 42 | 43 | func (r *RedisBackend) Connect() error { 44 | redisLogger.Info("Creating new Redis connection pool") 45 | 46 | conf := r.config 47 | optsR := model.RedisOptions{ 48 | Username: conf.Username, 49 | Password: conf.Password, 50 | Host: conf.Host, 51 | Port: conf.Port, 52 | Timeout: conf.Timeout, 53 | Hosts: conf.Hosts, 54 | Addrs: conf.Addrs, 55 | MasterName: conf.MasterName, 56 | SentinelPassword: conf.SentinelPassword, 57 | Database: conf.Database, 58 | MaxActive: conf.MaxActive, 59 | EnableCluster: conf.EnableCluster, 60 | } 61 | 62 | tls := model.TLS{ 63 | Enable: conf.UseSSL, 64 | InsecureSkipVerify: conf.SSLInsecureSkipVerify, 65 | CAFile: conf.CAFile, 66 | CertFile: conf.CertFile, 67 | KeyFile: conf.KeyFile, 68 | MinVersion: conf.MinVersion, 69 | MaxVersion: conf.MaxVersion, 70 | } 71 | 72 | connector, err := connector.NewConnector(model.RedisV9Type, model.WithRedisConfig(&optsR), model.WithTLS(&tls)) 73 | if err != nil { 74 | redisLogger.WithError(err).Error("creating redis connector") 75 | return err 76 | } 77 | 78 | r.kv, err = temporal.NewKeyValue(connector) 79 | if err != nil { 80 | redisLogger.WithError(err).Error("creating KV store") 81 | return err 82 | } 83 | 84 | return nil 85 | } 86 | 87 | // Init will create the initial in-memory store structures 88 | func (r *RedisBackend) Init(config interface{}) error { 89 | asJ, err := json.Marshal(config) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | fixedConf := RedisConfig{} 95 | err = json.Unmarshal(asJ, &fixedConf) 96 | if err != nil { 97 | return err 98 | } 99 | r.config = &fixedConf 100 | err = r.Connect() 101 | if err != nil { 102 | return err 103 | } 104 | 105 | redisLogger.Info("Initialized") 106 | return nil 107 | } 108 | 109 | // SetDb from existent connection 110 | func (r *RedisBackend) SetDb(kv temporal.KeyValue) { 111 | logger = log.Get() 112 | redisLogger = &logrus.Entry{Logger: logger} 113 | redisLogger = redisLogger.Logger.WithField("prefix", "TIB REDIS STORE") 114 | 115 | r.kv = kv 116 | redisLogger.Info("Set KV store") 117 | } 118 | 119 | func toJSONString(val interface{}) (string, error) { 120 | jsonBytes, err := json.Marshal(val) 121 | if err != nil { 122 | return "", err // Return an error if marshaling fails 123 | } 124 | return string(jsonBytes), nil // Convert byte slice to string 125 | } 126 | 127 | func (r *RedisBackend) SetKey(key string, orgId string, val interface{}) error { 128 | var err error 129 | strVal, ok := val.(string) 130 | if !ok { 131 | // Handle the case where val is not a string. Convert it via json marshaller 132 | strVal, err = toJSONString(val) 133 | if err != nil { 134 | redisLogger.WithError(err).Error("cannot store interface in redis") 135 | return err 136 | } 137 | } 138 | 139 | if err = r.kv.Set(ctx, r.fixKey(key), strVal, 0); err != nil { 140 | redisLogger.WithError(err).Debug("Error trying to set value") 141 | return err 142 | } 143 | 144 | return nil 145 | } 146 | 147 | func (r *RedisBackend) GetKey(key string, orgId string, val interface{}) error { 148 | result, err := r.kv.Get(ctx, r.fixKey(key)) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | // if AuthConfigStore is redis adapter, then redis return string 154 | if err = json.Unmarshal([]byte(result), &val); err != nil { 155 | redisLogger.WithError(err).Error("unmarshalling redis result into interface") 156 | } 157 | 158 | return err 159 | } 160 | 161 | // GetKeys will return all keys according to the filter (filter is a prefix - e.g. tyk.keys.*) 162 | func (r *RedisBackend) GetKeys(filter string) []string { 163 | keys, err := r.kv.Keys(ctx, filter) 164 | if err != nil { 165 | redisLogger.WithError(err).Error("getting keys") 166 | } 167 | 168 | for k, v := range keys { 169 | keys[k] = r.cleanKey(v) 170 | } 171 | return keys 172 | } 173 | 174 | func (r *RedisBackend) GetAll(orgId string) []interface{} { 175 | 176 | keys, err := r.kv.Keys(ctx, r.KeyPrefix) 177 | if err != nil { 178 | redisLogger.WithError(err).Error("retrieving keys from redis") 179 | return nil 180 | } 181 | 182 | if keys == nil { 183 | logger.Error("Error trying to get filtered client keys") 184 | return nil 185 | } 186 | 187 | if len(keys) == 0 { 188 | return nil 189 | } 190 | 191 | for i, v := range keys { 192 | keys[i] = r.KeyPrefix + v 193 | } 194 | 195 | var values []interface{} = make([]interface{}, len(keys)) 196 | for i, s := range keys { 197 | values[i] = s 198 | } 199 | return values 200 | } 201 | 202 | func (r *RedisBackend) cleanKey(keyName string) string { 203 | return strings.Replace(keyName, r.KeyPrefix, "", 1) 204 | } 205 | 206 | func (r *RedisBackend) DeleteKey(key string, orgId string) error { 207 | return r.kv.Delete(ctx, r.fixKey(key)) 208 | } 209 | 210 | func (r *RedisBackend) fixKey(keyName string) string { 211 | return r.KeyPrefix + keyName 212 | } 213 | -------------------------------------------------------------------------------- /ci/goreleaser/.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Generated by: tyk-ci/wf-gen 2 | # Generated on: Fri Jan 14 17:45:41 UTC 2022 3 | 4 | # Generation commands: 5 | # ./pr.zsh -base v1.2.2-rc5 -branch v1.2.2-rc5-m4-sync -title sync m4 templates -repos tyk-identity-broker 6 | # m4 -E -DxREPO=tyk-identity-broker 7 | 8 | 9 | # Check the documentation at http://goreleaser.com 10 | # This project needs CGO_ENABLED=1 and the cross-compiler toolchains for 11 | # - arm64 12 | # - macOS (only 10.15 is supported) 13 | # - amd64 14 | 15 | 16 | builds: 17 | - id: std 18 | ldflags: 19 | - -X main.VERSION={{.Version}} -X main.commit={{.FullCommit}} -X main.buildDate={{.Date}} -X main.builtBy=goreleaser 20 | goos: 21 | - linux 22 | - darwin 23 | goarch: 24 | - amd64 25 | - arm64 26 | # static builds strip symbols and do not allow plugins 27 | - id: static-amd64 28 | ldflags: 29 | - -s -w -X main.VERSION={{.Version}} -X main.commit={{.FullCommit}} -X main.buildDate={{.Date}} -X main.builtBy=goreleaser 30 | goos: 31 | - linux 32 | goarch: 33 | - amd64 34 | 35 | 36 | dockers: 37 | # Build tykio/xDH_REPO, cloudsmith/xCOMPATIBILITY_NAME (amd64) 38 | - ids: 39 | - std 40 | image_templates: 41 | - "tykio/tyk-identity-broker:{{ .Tag }}-amd64" 42 | - "docker.tyk.io/tyk-identity-broker/tyk-identity-broker:{{ .Tag }}" 43 | build_flag_templates: 44 | - "--build-arg=PORTS=80" 45 | - "--platform=linux/amd64" 46 | - "--label=org.opencontainers.image.created={{.Date}}" 47 | - "--label=org.opencontainers.image.title={{.ProjectName}}" 48 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 49 | - "--label=org.opencontainers.image.version={{.Version}}" 50 | use: buildx 51 | goarch: amd64 52 | goos: linux 53 | dockerfile: Dockerfile.std 54 | extra_files: 55 | - "install/" 56 | - "README.md" 57 | 58 | # Build tykio/xDH_REPO, cloudsmith/xCOMPATIBILITY_NAME (arm64) 59 | - ids: 60 | - std 61 | image_templates: 62 | - "tykio/tyk-identity-broker:{{ .Tag }}-arm64" 63 | - "docker.tyk.io/tyk-identity-broker/tyk-identity-broker:{{ .Tag }}-arm64" 64 | build_flag_templates: 65 | - "--build-arg=PORTS=80" 66 | - "--platform=linux/arm64" 67 | - "--label=org.opencontainers.image.created={{.Date}}" 68 | - "--label=org.opencontainers.image.title={{.ProjectName}}-arm64" 69 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 70 | - "--label=org.opencontainers.image.version={{.Version}}" 71 | use: buildx 72 | goarch: arm64 73 | goos: linux 74 | dockerfile: Dockerfile.std 75 | extra_files: 76 | - "install/" 77 | - "README.md" 78 | 79 | - ids: 80 | - static-amd64 81 | image_templates: 82 | - "tykio/tyk-identity-broker:s{{ .Version }}" 83 | - "tykio/tyk-identity-broker:s{{ .Major }}.{{ .Minor }}" 84 | - "docker.tyk.io/tyk-identity-broker/tyk-identity-broker:s{{ .Version }}" 85 | - "docker.tyk.io/tyk-identity-broker/tyk-identity-broker:s{{ .Major }}.{{ .Minor }}" 86 | build_flag_templates: 87 | - "--build-arg=PORTS=80" 88 | - "--label=org.opencontainers.image.created={{.Date}}" 89 | - "--label=org.opencontainers.image.title={{.ProjectName}}-slim" 90 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 91 | - "--label=org.opencontainers.image.version={{.Version}}" 92 | goarch: amd64 93 | goos: linux 94 | dockerfile: Dockerfile.slim 95 | extra_files: 96 | - "install/" 97 | - "README.md" 98 | 99 | 100 | docker_manifests: 101 | - name_template: tykio/tyk-identity-broker:{{ .Tag }} 102 | image_templates: 103 | - tykio/tyk-identity-broker:{{ .Tag }}-amd64 104 | - tykio/tyk-identity-broker:{{ .Tag }}-arm64 105 | - name_template: tykio/tyk-identity-broker:v{{ .Major }}.{{ .Minor }}{{.Prerelease}} 106 | image_templates: 107 | - tykio/tyk-identity-broker:{{ .Tag }}-amd64 108 | - tykio/tyk-identity-broker:{{ .Tag }}-arm64 109 | 110 | 111 | nfpms: 112 | - id: std 113 | vendor: "Tyk Technologies Ltd" 114 | homepage: "https://tyk.io" 115 | maintainer: "Tyk " 116 | description: 117 | package_name: tyk-identity-broker 118 | builds: 119 | 120 | - std 121 | formats: 122 | - deb 123 | - rpm 124 | contents: 125 | - src: "README.md" 126 | dst: "/opt/share/docs/tyk-identity-broker/README.md" 127 | - src: "install/*" 128 | dst: "/opt/tyk-identity-broker/install" 129 | - src: install/inits/systemd/system/tyk-identity-broker.service 130 | dst: /lib/systemd/system/tyk-identity-broker.service 131 | - src: install/inits/sysv/init.d/tyk-identity-broker 132 | dst: /etc/init.d/tyk-identity-broker 133 | 134 | - src: "LICENSE.md" 135 | dst: "/opt/share/docs/tyk-identity-broker/LICENSE.md" 136 | - src: tib_sample.conf 137 | dst: /opt/tyk-identity-broker/tib.conf 138 | type: "config|noreplace" 139 | scripts: 140 | preinstall: "install/before_install.sh" 141 | postinstall: "install/post_install.sh" 142 | postremove: "install/post_remove.sh" 143 | bindir: "/opt/tyk-identity-broker" 144 | overrides: 145 | rpm: 146 | replacements: 147 | amd64: x86_64 148 | arm: aarch64 149 | deb: 150 | replacements: 151 | arm: arm64 152 | rpm: 153 | scripts: 154 | posttrans: install/post_trans.sh 155 | signature: 156 | key_file: tyk.io.signing.key 157 | deb: 158 | signature: 159 | key_file: tyk.io.signing.key 160 | type: origin 161 | 162 | 163 | 164 | archives: 165 | - id: std-linux 166 | builds: 167 | 168 | - std 169 | files: 170 | - README.md 171 | - "install/*" 172 | 173 | - id: static-amd64 174 | name_template: "{{ .ProjectName }}_{{ .Version }}_static_{{ .Os }}_{{ .Arch }}" 175 | builds: 176 | - static-amd64 177 | files: 178 | - README.md 179 | 180 | 181 | 182 | checksum: 183 | disable: false 184 | 185 | signs: 186 | - id: std 187 | artifacts: checksum 188 | 189 | changelog: 190 | sort: asc 191 | filters: 192 | exclude: 193 | - '^utils:' 194 | - (?i)typo 195 | - 'Merge (pull request|branch)' 196 | - '\[CI\]' 197 | - '(?i)\[Buddy\]' 198 | - 'cherry picked' 199 | - '^rel-eng:' 200 | - '^minor:' 201 | 202 | release: 203 | github: 204 | owner: TykTechnologies 205 | name: tyk-identity-broker 206 | prerelease: auto 207 | draft: true 208 | name_template: "{{.ProjectName}}-v{{.Version}}" 209 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/TykTechnologies/tyk-identity-broker 2 | 3 | go 1.21.11 4 | 5 | toolchain go1.22.2 6 | 7 | require ( 8 | github.com/Jeffail/gabs v1.4.0 9 | github.com/TykTechnologies/storage v1.2.2 10 | github.com/TykTechnologies/tyk v1.9.2-0.20240815043856-ec7db94fbe3d 11 | github.com/crewjam/saml v0.4.14 12 | github.com/go-jose/go-jose/v3 v3.0.3 13 | github.com/go-ldap/ldap/v3 v3.2.3 14 | github.com/gofrs/uuid v4.4.0+incompatible 15 | github.com/gorilla/mux v1.8.1 16 | github.com/gorilla/sessions v1.2.1 17 | github.com/kelseyhightower/envconfig v1.4.0 18 | github.com/markbates/goth v1.64.2 19 | github.com/matryer/is v1.4.0 20 | github.com/sirupsen/logrus v1.9.3 21 | github.com/stretchr/testify v1.9.0 22 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 23 | go.mongodb.org/mongo-driver v1.13.1 24 | golang.org/x/oauth2 v0.18.0 25 | golang.org/x/text v0.21.0 26 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 27 | ) 28 | 29 | require ( 30 | github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect 31 | github.com/Masterminds/goutils v1.1.1 // indirect 32 | github.com/Masterminds/semver v1.5.0 // indirect 33 | github.com/Masterminds/sprig v2.22.0+incompatible // indirect 34 | github.com/TykTechnologies/gojsonschema v0.0.0-20170222154038-dcb3e4bb7990 // indirect 35 | github.com/TykTechnologies/graphql-go-tools v1.6.2-0.20240705065952-ae6008677a48 // indirect 36 | github.com/TykTechnologies/murmur3 v0.0.0-20230310161213-aad17efd5632 // indirect 37 | github.com/TykTechnologies/opentelemetry v0.0.21 // indirect 38 | github.com/beevik/etree v1.2.0 // indirect 39 | github.com/buger/jsonparser v1.1.1 // indirect 40 | github.com/cenk/backoff v2.2.1+incompatible // indirect 41 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 42 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 43 | github.com/clbanning/mxj v1.8.4 // indirect 44 | github.com/crewjam/httperr v0.2.0 // indirect 45 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 46 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 47 | github.com/eclipse/paho.mqtt.golang v1.2.0 // indirect 48 | github.com/felixge/httpsnoop v1.0.3 // indirect 49 | github.com/go-asn1-ber/asn1-ber v1.5.1 // indirect 50 | github.com/go-logr/logr v1.3.0 // indirect 51 | github.com/go-logr/stdr v1.2.2 // indirect 52 | github.com/go-redis/redismock/v9 v9.2.0 // indirect 53 | github.com/go-redsync/redsync/v4 v4.11.0 // indirect 54 | github.com/golang-jwt/jwt/v4 v4.5.0 // indirect 55 | github.com/golang/protobuf v1.5.4 // indirect 56 | github.com/golang/snappy v0.0.4 // indirect 57 | github.com/google/uuid v1.6.0 // indirect 58 | github.com/gorilla/securecookie v1.1.1 // indirect 59 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect 60 | github.com/huandu/xstrings v1.3.3 // indirect 61 | github.com/imdario/mergo v0.3.12 // indirect 62 | github.com/jensneuse/abstractlogger v0.0.4 // indirect 63 | github.com/jensneuse/pipeline v0.0.0-20200117120358-9fb4de085cd6 // indirect 64 | github.com/jonboulle/clockwork v0.2.2 // indirect 65 | github.com/klauspost/compress v1.17.9 // indirect 66 | github.com/lonelycode/go-uuid v0.0.0-20141202165402-ed3ca8a15a93 // indirect 67 | github.com/lonelycode/osin v0.0.0-20160423095202-da239c9dacb6 // indirect 68 | github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect 69 | github.com/mattn/go-colorable v0.1.13 // indirect 70 | github.com/mattn/go-isatty v0.0.20 // indirect 71 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 72 | github.com/mitchellh/copystructure v1.2.0 // indirect 73 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 74 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect 75 | github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c // indirect 76 | github.com/onsi/gomega v1.27.10 // indirect 77 | github.com/opentracing/opentracing-go v1.2.0 // indirect 78 | github.com/pkg/errors v0.9.1 // indirect 79 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 80 | github.com/pmylund/go-cache v2.1.0+incompatible // indirect 81 | github.com/redis/go-redis/v9 v9.5.3 // indirect 82 | github.com/russellhaering/goxmldsig v1.4.0 // indirect 83 | github.com/stretchr/objx v0.5.2 // indirect 84 | github.com/tidwall/gjson v1.11.0 // indirect 85 | github.com/tidwall/match v1.1.1 // indirect 86 | github.com/tidwall/pretty v1.2.0 // indirect 87 | github.com/tidwall/sjson v1.0.4 // indirect 88 | github.com/uber/jaeger-client-go v2.30.1-0.20220110192849-8d8e8fcfd04d+incompatible // indirect 89 | github.com/uber/jaeger-lib v2.4.2-0.20210604143007-135cf5605a6d+incompatible // indirect 90 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect 91 | github.com/xdg-go/scram v1.1.2 // indirect 92 | github.com/xdg-go/stringprep v1.0.4 // indirect 93 | github.com/xeipuuv/gojsonpointer v0.0.0-20190809123943-df4f5c81cb3b // indirect 94 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 95 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect 96 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect 97 | go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect 98 | go.opentelemetry.io/otel v1.19.0 // indirect 99 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 // indirect 100 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.18.0 // indirect 101 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0 // indirect 102 | go.opentelemetry.io/otel/metric v1.19.0 // indirect 103 | go.opentelemetry.io/otel/sdk v1.18.0 // indirect 104 | go.opentelemetry.io/otel/trace v1.19.0 // indirect 105 | go.opentelemetry.io/proto/otlp v1.0.0 // indirect 106 | go.uber.org/atomic v1.11.0 // indirect 107 | go.uber.org/multierr v1.11.0 // indirect 108 | go.uber.org/zap v1.21.0 // indirect 109 | golang.org/x/crypto v0.31.0 // indirect 110 | golang.org/x/net v0.26.0 // indirect 111 | golang.org/x/sync v0.10.0 // indirect 112 | golang.org/x/sys v0.28.0 // indirect 113 | golang.org/x/term v0.27.0 // indirect 114 | google.golang.org/appengine v1.6.8 // indirect 115 | google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect 116 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect 117 | google.golang.org/grpc v1.64.0 // indirect 118 | google.golang.org/protobuf v1.34.2 // indirect 119 | gopkg.in/yaml.v3 v3.0.1 // indirect 120 | ) 121 | 122 | replace github.com/jeffail/tunny => github.com/Jeffail/tunny v0.0.0-20171107125207-452a8e97d6a3 123 | 124 | replace github.com/jensneuse/graphql-go-tools => github.com/TykTechnologies/graphql-go-tools v1.6.2-0.20211213120648-56cd4003725b 125 | 126 | replace gorm.io/gorm => github.com/TykTechnologies/gorm v1.20.7-0.20210409171139-b5c340f85ed0 127 | 128 | replace github.com/crewjam/saml => github.com/TykTechnologies/saml v0.4.6-0.20231025135323-7acac1a634e0 129 | 130 | exclude github.com/TykTechnologies/tyk/certs v0.0.1 131 | -------------------------------------------------------------------------------- /providers/social.go: -------------------------------------------------------------------------------- 1 | /* 2 | package providers is a catch-all for all TAP auth provider types (e.g. social, active directory), if you are 3 | 4 | extending TAP to use more providers, add them to this section 5 | */ 6 | package providers 7 | 8 | import ( 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "sync" 13 | 14 | "github.com/TykTechnologies/tyk-identity-broker/internal/jwe" 15 | "github.com/TykTechnologies/tyk/certs" 16 | 17 | "net/http" 18 | "strings" 19 | 20 | "github.com/markbates/goth" 21 | "github.com/markbates/goth/providers/bitbucket" 22 | "github.com/markbates/goth/providers/digitalocean" 23 | "github.com/markbates/goth/providers/dropbox" 24 | "github.com/markbates/goth/providers/github" 25 | "github.com/markbates/goth/providers/gplus" 26 | "github.com/markbates/goth/providers/linkedin" 27 | "github.com/markbates/goth/providers/openidConnect" 28 | "github.com/markbates/goth/providers/salesforce" 29 | "github.com/markbates/goth/providers/twitter" 30 | "golang.org/x/oauth2" 31 | 32 | "github.com/TykTechnologies/tyk-identity-broker/tap" 33 | "github.com/TykTechnologies/tyk-identity-broker/toth" 34 | "github.com/TykTechnologies/tyk-identity-broker/tothic" 35 | "github.com/sirupsen/logrus" 36 | 37 | logger "github.com/TykTechnologies/tyk-identity-broker/log" 38 | ) 39 | 40 | var log = logger.Get() 41 | 42 | // SocialLogTag is the log tag for the social provider 43 | var SocialLogTag = "SOCIAL AUTH" 44 | var onceReloadSocialLogger sync.Once 45 | var socialLogger = log.WithField("prefix", SocialLogTag) 46 | 47 | // Social is the identity handler for all social auth, it is a wrapper around Goth, and makes use of it's pluggable 48 | // providers to provide a raft of social OAuth providers as SSO or Login delegates. 49 | type Social struct { 50 | handler tap.IdentityHandler 51 | config GothConfig 52 | toth toth.TothInstance 53 | profile tap.Profile 54 | } 55 | 56 | // GothProviderConfig the configurations required for the individual goth providers 57 | type GothProviderConfig struct { 58 | Name string 59 | Key string 60 | Secret string 61 | DiscoverURL string 62 | DisableAuthHeaderProviderDomain string 63 | Scopes []string 64 | SkipUserInfoRequest bool 65 | } 66 | 67 | // GothConfig is the main configuration object for the Social provider 68 | type GothConfig struct { 69 | UseProviders []GothProviderConfig 70 | CallbackBaseURL string 71 | FailureRedirect string 72 | JWE jwe.Handler `json:"JWE,omitempty"` 73 | } 74 | 75 | // Name returns the name of the provider 76 | func (s *Social) Name() string { 77 | return "SocialProvider" 78 | } 79 | 80 | // ProviderType returns the type of the provider, Social makes use of the reirect type, as 81 | // it redirects the user to multiple locations in the flow 82 | func (s *Social) ProviderType() tap.ProviderType { 83 | return tap.REDIRECT_PROVIDER 84 | } 85 | 86 | // UseCallback returns whether or not the callback URL is used for this profile. Social uses it. 87 | func (s *Social) UseCallback() bool { 88 | return true 89 | } 90 | 91 | // Init will configure the social provider for this request. 92 | func (s *Social) Init(handler tap.IdentityHandler, profile tap.Profile, config []byte) error { 93 | //if an external logger was set, then lets reload it to inherit those configs 94 | onceReloadADLogger.Do(func() { 95 | log = logger.Get() 96 | socialLogger = &logrus.Entry{Logger: log} 97 | socialLogger = socialLogger.Logger.WithField("prefix", SocialLogTag) 98 | }) 99 | 100 | s.handler = handler 101 | s.profile = profile 102 | s.toth = toth.TothInstance{} 103 | s.toth.Init() 104 | 105 | unmarshallErr := json.Unmarshal(config, &s.config) 106 | if unmarshallErr != nil { 107 | return unmarshallErr 108 | } 109 | 110 | if s.config.JWE.Enabled { 111 | keys := CertManager.List([]string{s.config.JWE.PrivateKeyLocation}, certs.CertificateAny) 112 | if len(keys) == 0 { 113 | socialLogger.Error("JWE Private Key was not loaded") 114 | } else { 115 | socialLogger.Debug("JWE Private Key Loaded") 116 | s.config.JWE.Key = keys[0] 117 | } 118 | } 119 | 120 | // TODO: Add more providers here 121 | gothProviders := []goth.Provider{} 122 | for _, provider := range s.config.UseProviders { 123 | switch provider.Name { 124 | case "gplus": 125 | gothProviders = append(gothProviders, gplus.New(provider.Key, provider.Secret, s.getCallBackURL(provider.Name))) 126 | 127 | case "github": 128 | gothProviders = append(gothProviders, github.New(provider.Key, provider.Secret, s.getCallBackURL(provider.Name))) 129 | 130 | case "twitter": 131 | gothProviders = append(gothProviders, twitter.New(provider.Key, provider.Secret, s.getCallBackURL(provider.Name))) 132 | 133 | case "linkedin": 134 | gothProviders = append(gothProviders, linkedin.New(provider.Key, provider.Secret, s.getCallBackURL(provider.Name))) 135 | 136 | case "dropbox": 137 | gothProviders = append(gothProviders, dropbox.New(provider.Key, provider.Secret, s.getCallBackURL(provider.Name))) 138 | 139 | case "digitalocean": 140 | gothProviders = append(gothProviders, digitalocean.New(provider.Key, provider.Secret, s.getCallBackURL(provider.Name))) 141 | 142 | case "bitbucket": 143 | gothProviders = append(gothProviders, bitbucket.New(provider.Key, provider.Secret, s.getCallBackURL(provider.Name))) 144 | 145 | case "salesforce": 146 | gothProviders = append(gothProviders, salesforce.New(provider.Key, provider.Secret, s.getCallBackURL(provider.Name))) 147 | 148 | case "openid-connect": 149 | 150 | gProv, err := openidConnect.New(provider.Key, provider.Secret, s.getCallBackURL(provider.Name), provider.DiscoverURL, provider.Scopes...) 151 | if err != nil { 152 | socialLogger.Error(err) 153 | return err 154 | } 155 | 156 | gProv.SkipUserInfoRequest = provider.SkipUserInfoRequest 157 | 158 | // See https://godoc.org/golang.org/x/oauth2#RegisterBrokenAuthHeaderProvider 159 | if provider.DisableAuthHeaderProviderDomain != "" { 160 | oauth2.RegisterBrokenAuthHeaderProvider(provider.DisableAuthHeaderProviderDomain) 161 | } 162 | 163 | gothProviders = append(gothProviders, gProv) 164 | } 165 | } 166 | 167 | s.toth.UseProviders(gothProviders...) 168 | return nil 169 | } 170 | 171 | // Handle is the main callback delegate for the generic auth flow 172 | func (s *Social) Handle(w http.ResponseWriter, r *http.Request, pathParams map[string]string, profile tap.Profile) { 173 | tothic.BeginAuthHandler(w, r, &s.toth, pathParams, profile) 174 | } 175 | 176 | func (s *Social) checkConstraints(user interface{}) error { 177 | var thisUser goth.User 178 | thisUser = user.(goth.User) 179 | 180 | if s.profile.ProviderConstraints.Domain != "" { 181 | if !strings.HasSuffix(thisUser.Email, s.profile.ProviderConstraints.Domain) { 182 | return errors.New("domain constraint failed, user domain does not match profile") 183 | } 184 | } 185 | 186 | if s.profile.ProviderConstraints.Group != "" { 187 | socialLogger.Warning("Social Auth does not support Group constraints") 188 | } 189 | 190 | return nil 191 | } 192 | 193 | // HandleCallback handles the callback from the OAuth provider 194 | func (s *Social) HandleCallback(w http.ResponseWriter, r *http.Request, onError func(tag string, errorMsg string, rawErr error, code int, w http.ResponseWriter, r *http.Request), profile tap.Profile) { 195 | user, err := tothic.CompleteUserAuth(w, r, &s.toth, profile, &s.config.JWE) 196 | if err != nil { 197 | fmt.Fprintln(w, err) 198 | return 199 | } 200 | 201 | constraintErr := s.checkConstraints(user) 202 | if constraintErr != nil { 203 | if s.config.FailureRedirect == "" { 204 | onError(SocialLogTag, "Constraint failed", constraintErr, 400, w, r) 205 | return 206 | } 207 | 208 | http.Redirect(w, r, s.config.FailureRedirect, 301) 209 | return 210 | } 211 | 212 | //Todo set the user's email here, befotr going back to the handler 213 | // Complete login and redirect 214 | s.handler.CompleteIdentityAction(w, r, user, s.profile) 215 | } 216 | 217 | func (s *Social) getCallBackURL(provider string) string { 218 | return s.config.CallbackBaseURL + "/auth/" + s.profile.ID + "/" + provider + "/callback" 219 | } 220 | 221 | func (s *Social) HandleMetadata(http.ResponseWriter, *http.Request) { 222 | socialLogger.Warning("metadata not implemented for provider") 223 | } 224 | -------------------------------------------------------------------------------- /backends/redis_test.go: -------------------------------------------------------------------------------- 1 | package backends 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "testing" 7 | "time" 8 | 9 | "github.com/TykTechnologies/tyk-identity-broker/tap" 10 | 11 | mocks "github.com/TykTechnologies/storage/temporal/tempmocks" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/mock" 14 | ) 15 | 16 | func mockRedisBackend(t *testing.T) (*RedisBackend, *mocks.KeyValue) { 17 | testObj := mocks.NewKeyValue(t) 18 | rb := &RedisBackend{ 19 | kv: testObj, 20 | config: &RedisConfig{}, 21 | KeyPrefix: "key-prefix", 22 | } 23 | return rb, testObj 24 | } 25 | 26 | func TestConnect(t *testing.T) { 27 | testObj := mocks.NewKeyValue(t) 28 | 29 | rb := RedisBackend{ 30 | kv: testObj, 31 | config: &RedisConfig{}, 32 | } 33 | 34 | err := rb.Connect() 35 | 36 | assert.Nil(t, err) 37 | assert.NotNil(t, rb.kv, "key-value store should not be nil") 38 | } 39 | 40 | // TestCleanKey tests the cleanKey function 41 | func TestCleanKey(t *testing.T) { 42 | r, _ := mockRedisBackend(t) 43 | r.KeyPrefix = "prefix_" 44 | tests := []struct { 45 | keyName string 46 | want string 47 | }{ 48 | {"prefix_key1", "key1"}, 49 | {"prefix_key2", "key2"}, 50 | {"key3", "key3"}, 51 | } 52 | 53 | for _, tt := range tests { 54 | t.Run(tt.keyName, func(t *testing.T) { 55 | if got := r.cleanKey(tt.keyName); got != tt.want { 56 | t.Errorf("cleanKey() = %v, want %v", got, tt.want) 57 | } 58 | }) 59 | } 60 | } 61 | 62 | // TestFixKey tests the fixKey function 63 | func TestFixKey(t *testing.T) { 64 | r, _ := mockRedisBackend(t) 65 | r.KeyPrefix = "prefix_" 66 | tests := []struct { 67 | keyName string 68 | want string 69 | }{ 70 | {"key1", "prefix_key1"}, 71 | {"key2", "prefix_key2"}, 72 | } 73 | 74 | for _, tt := range tests { 75 | t.Run(tt.keyName, func(t *testing.T) { 76 | if got := r.fixKey(tt.keyName); got != tt.want { 77 | t.Errorf("fixKey() = %v, want %v", got, tt.want) 78 | } 79 | }) 80 | } 81 | } 82 | 83 | func TestRedisInit(t *testing.T) { 84 | 85 | testCases := []struct { 86 | name string 87 | config interface{} 88 | shouldErr bool 89 | }{ 90 | { 91 | name: "invalid config - numeric", 92 | config: 1111, 93 | shouldErr: true, 94 | }, 95 | { 96 | name: "invalid config - random string", 97 | config: "some-invalid-config", 98 | shouldErr: true, 99 | }, 100 | { 101 | name: "valid config", 102 | // change some configs 103 | config: RedisConfig{ 104 | MaxIdle: 1, 105 | MaxActive: 0, 106 | MasterName: "some-master", 107 | Database: 1, 108 | Username: "testUser", 109 | Password: "s3cr3t", 110 | UseSSL: true, 111 | SSLInsecureSkipVerify: true, 112 | Port: 5000, 113 | MaxVersion: "1.0", 114 | MinVersion: "1.0", 115 | }, 116 | shouldErr: false, 117 | }, 118 | { 119 | name: "unvalid TLS MAX/Min Version", 120 | // change some configs 121 | config: RedisConfig{ 122 | MaxIdle: 1, 123 | MaxActive: 0, 124 | MasterName: "some-master", 125 | Database: 1, 126 | Username: "testUser", 127 | Password: "s3cr3t", 128 | UseSSL: true, 129 | SSLInsecureSkipVerify: true, 130 | Port: 5000, 131 | MaxVersion: "xxx", 132 | MinVersion: "yyy", 133 | }, 134 | shouldErr: true, 135 | }, 136 | { 137 | name: "Min version is greater than max version", 138 | // change some configs 139 | config: RedisConfig{ 140 | MaxIdle: 1, 141 | MaxActive: 0, 142 | MasterName: "some-master", 143 | Database: 1, 144 | Username: "testUser", 145 | Password: "s3cr3t", 146 | UseSSL: true, 147 | SSLInsecureSkipVerify: true, 148 | Port: 5000, 149 | MaxVersion: "1.0", 150 | MinVersion: "1.2", 151 | }, 152 | shouldErr: true, 153 | }, 154 | { 155 | name: "invalid config - non marshable", 156 | config: make(chan int), 157 | shouldErr: true, 158 | }, 159 | } 160 | 161 | for _, tc := range testCases { 162 | t.Run(tc.name, func(t *testing.T) { 163 | r, _ := mockRedisBackend(t) 164 | 165 | err := r.Init(tc.config) 166 | 167 | didErr := err != nil 168 | assert.Equal(t, tc.shouldErr, didErr) 169 | 170 | if !tc.shouldErr { 171 | assert.Equal(t, tc.config, *r.config) 172 | } 173 | }) 174 | } 175 | } 176 | 177 | func TestRedisBackend_SetDb(t *testing.T) { 178 | // Mock KeyValue instance 179 | testObj := mocks.NewKeyValue(t) 180 | testObj.Test(t) 181 | 182 | // Create an instance of RedisBackend 183 | r := &RedisBackend{ 184 | // Initialize other necessary fields, if any 185 | } 186 | 187 | // Call SetDb with the mock KeyValue 188 | r.SetDb(testObj) 189 | 190 | // Assertions 191 | assert.Equal(t, testObj, r.kv, "KeyValue instance not set correctly in RedisBackend") 192 | } 193 | 194 | func TestRedis_SetKey(t *testing.T) { 195 | rb, testObj := mockRedisBackend(t) 196 | 197 | keyName := "key" 198 | orgId := "orgId" 199 | value := "test-val" 200 | var ttl time.Duration 201 | 202 | testObj.On("Set", mock.Anything, rb.KeyPrefix+keyName, value, ttl).Return(nil) 203 | err := rb.SetKey(keyName, orgId, value) 204 | assert.Nil(t, err) 205 | testObj.AssertExpectations(t) 206 | } 207 | 208 | func TestRedis_GetKey(t *testing.T) { 209 | // Setting up mocks 210 | rb, testObj := mockRedisBackend(t) 211 | 212 | // Preparing test data 213 | testProfile := tap.Profile{ 214 | ID: "some-profile", 215 | OrgID: "test-org", 216 | } 217 | 218 | bytes, err := json.Marshal(testProfile) 219 | assert.Nil(t, err) 220 | 221 | keyName := "key" 222 | orgId := "orgId" 223 | value := string(bytes) 224 | var ttl time.Duration 225 | 226 | // Setting up expectations for the mock object 227 | testObj.On("Set", mock.Anything, rb.KeyPrefix+keyName, value, ttl).Return(nil) 228 | testObj.On("Get", mock.Anything, rb.KeyPrefix+keyName).Return(value, nil) 229 | 230 | // Executing the function under test 231 | err = rb.SetKey(keyName, orgId, value) 232 | assert.Nil(t, err) 233 | 234 | var newVal tap.Profile 235 | err = rb.GetKey(keyName, orgId, &newVal) 236 | assert.Nil(t, err) 237 | 238 | // Verifying that expectations were met 239 | testObj.AssertExpectations(t) 240 | } 241 | 242 | func TestRedis_DeleteKey(t *testing.T) { 243 | rb, testObj := mockRedisBackend(t) 244 | key := "keyName" 245 | orgId := "orgId" 246 | 247 | testObj.On("Delete", mock.Anything, rb.KeyPrefix+key).Return(nil) 248 | 249 | err := rb.DeleteKey(key, orgId) 250 | assert.Nil(t, err) 251 | testObj.AssertExpectations(t) 252 | } 253 | 254 | func TestRedis_GetAll(t *testing.T) { 255 | rb, testObj := mockRedisBackend(t) 256 | 257 | orgId := "test-org" 258 | 259 | testObj.On("Keys", mock.Anything, rb.KeyPrefix).Return([]string{}, errors.New("pulling keys")) 260 | keys := rb.GetAll(orgId) 261 | assert.Len(t, keys, 0) 262 | } 263 | 264 | // Test for toJSONString function 265 | func TestToJSONString(t *testing.T) { 266 | tests := []struct { 267 | input interface{} 268 | expected string 269 | err bool 270 | }{ 271 | // Test cases with expected outputs 272 | {input: map[string]string{"key": "value"}, expected: `{"key":"value"}`, err: false}, 273 | {input: []int{1, 2, 3}, expected: `[1,2,3]`, err: false}, 274 | {input: "Hello, world!", expected: `"Hello, world!"`, err: false}, 275 | {input: nil, expected: "null", err: false}, 276 | {input: struct{ Name string }{Name: "Alice"}, expected: `{"Name":"Alice"}`, err: false}, 277 | 278 | // Test case expected to fail (circular reference) 279 | {input: func() {}, expected: "", err: true}, // Function types cannot be marshaled 280 | } 281 | 282 | for _, test := range tests { 283 | result, err := toJSONString(test.input) 284 | 285 | if test.err { 286 | if err == nil { 287 | t.Errorf("Expected error for input %v, got nil", test.input) 288 | } 289 | } else { 290 | if err != nil { 291 | t.Errorf("Unexpected error for input %v: %v", test.input, err) 292 | } 293 | if result != test.expected { 294 | t.Errorf("Expected %s for input %v, got %s", test.expected, test.input, result) 295 | } 296 | } 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /tap/identity-handlers/tyk_handler_test.go: -------------------------------------------------------------------------------- 1 | package identityHandlers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/markbates/goth" 9 | ) 10 | 11 | const ( 12 | TestEmail = "test@tyk.io" 13 | TestId = "user-id" 14 | DefaultGroupId = "default-group-id" 15 | ) 16 | 17 | var UserGroupMapping = map[string]string{ 18 | "devs": "devs-group", 19 | "admins": "admins-group", 20 | "CN=tyk_admin,OU=Security Groups,OU=GenericOrg,DC=GenericOrg,DC=COM,DC=GEN": "tyk-admin", 21 | } 22 | 23 | func TestGetEmail(t *testing.T) { 24 | cases := []struct { 25 | TestName string 26 | CustomEmailField string 27 | user goth.User 28 | ExpectedEmail string 29 | }{ 30 | { 31 | TestName: "Custom email field empty & goth.User email not empty", 32 | CustomEmailField: "", 33 | user: goth.User{ 34 | Email: TestEmail, 35 | }, 36 | ExpectedEmail: TestEmail, 37 | }, 38 | { 39 | TestName: "Custom email empty & goth.User email empty", 40 | CustomEmailField: "", 41 | user: goth.User{ 42 | Email: "", 43 | }, 44 | ExpectedEmail: DefaultSSOEmail, 45 | }, 46 | { 47 | TestName: "Custom email not empty but field doesn't exist", 48 | CustomEmailField: "myEmailField", 49 | user: goth.User{}, 50 | ExpectedEmail: DefaultSSOEmail, 51 | }, 52 | { 53 | TestName: "Custom email not empty and is a valid field", 54 | CustomEmailField: "myEmailField", 55 | user: goth.User{ 56 | RawData: map[string]interface{}{ 57 | "myEmailField": TestEmail, 58 | }, 59 | }, 60 | ExpectedEmail: TestEmail, 61 | }, 62 | } 63 | 64 | for _, tc := range cases { 65 | t.Run(tc.TestName, func(t *testing.T) { 66 | email := GetEmail(tc.user, tc.CustomEmailField) 67 | if email != tc.ExpectedEmail { 68 | t.Errorf("Email for SSO incorrect. Expected:%v got:%v", tc.ExpectedEmail, email) 69 | } 70 | }) 71 | } 72 | } 73 | 74 | func TestGetUserID(t *testing.T) { 75 | cases := []struct { 76 | TestName string 77 | CustomIDField string 78 | user goth.User 79 | ExpectedID string 80 | }{ 81 | { 82 | TestName: "Custom id field empty", 83 | CustomIDField: "", 84 | user: goth.User{ 85 | UserID: TestId, 86 | }, 87 | ExpectedID: TestId, 88 | }, 89 | { 90 | TestName: "Custom id not empty but field doesn't exist", 91 | CustomIDField: "myIdField", 92 | user: goth.User{ 93 | UserID: TestId, 94 | }, 95 | ExpectedID: TestId, 96 | }, 97 | { 98 | TestName: "Custom id not empty and is a valid field", 99 | CustomIDField: "myIdField", 100 | user: goth.User{ 101 | UserID: TestId, 102 | RawData: map[string]interface{}{ 103 | "myIdField": "customId", 104 | }, 105 | }, 106 | ExpectedID: "customId", 107 | }, 108 | } 109 | 110 | for _, tc := range cases { 111 | t.Run(tc.TestName, func(t *testing.T) { 112 | id := GetUserID(tc.user, tc.CustomIDField) 113 | if id != tc.ExpectedID { 114 | t.Errorf("User id incorrect. Expected:%v got:%v", tc.ExpectedID, id) 115 | } 116 | }) 117 | } 118 | } 119 | 120 | func TestGetGroupId(t *testing.T) { 121 | cases := []struct { 122 | TestName string 123 | CustomGroupIDField string 124 | user goth.User 125 | ExpectedGroupsIDs []string 126 | DefaultGroupID string 127 | UserGroupMapping map[string]string 128 | UserGroupSeparator string 129 | }{ 130 | { 131 | TestName: "Custom group id field empty", 132 | CustomGroupIDField: "", 133 | user: goth.User{}, 134 | ExpectedGroupsIDs: []string{}, 135 | DefaultGroupID: "", 136 | UserGroupMapping: UserGroupMapping, 137 | }, 138 | { 139 | TestName: "Custom group id field empty & default group set", 140 | CustomGroupIDField: "", 141 | user: goth.User{}, 142 | ExpectedGroupsIDs: []string{DefaultGroupId}, 143 | DefaultGroupID: DefaultGroupId, 144 | UserGroupMapping: UserGroupMapping, 145 | }, 146 | { 147 | TestName: "Custom group id field not empty but invalid & default group set", 148 | CustomGroupIDField: "my-custom-group-id-field", 149 | user: goth.User{}, 150 | DefaultGroupID: DefaultGroupId, 151 | ExpectedGroupsIDs: []string{DefaultGroupId}, 152 | UserGroupMapping: UserGroupMapping, 153 | }, 154 | { 155 | TestName: "Custom group id field not empty but invalid & default group not set", 156 | CustomGroupIDField: "my-custom-group-id-field", 157 | user: goth.User{}, 158 | ExpectedGroupsIDs: []string{}, 159 | DefaultGroupID: "", 160 | UserGroupMapping: UserGroupMapping, 161 | }, 162 | { 163 | TestName: "Custom group id field not empty & valid. With default group not set", 164 | CustomGroupIDField: "my-custom-group-id-field", 165 | user: goth.User{ 166 | RawData: map[string]interface{}{ 167 | "my-custom-group-id-field": "admins", 168 | }, 169 | }, 170 | ExpectedGroupsIDs: []string{"admins-group"}, 171 | DefaultGroupID: "", 172 | UserGroupMapping: UserGroupMapping, 173 | }, 174 | { 175 | TestName: "Receive many groups from idp with blank space separated", 176 | CustomGroupIDField: "my-custom-group-id-field", 177 | user: goth.User{ 178 | RawData: map[string]interface{}{ 179 | "my-custom-group-id-field": "devs admins", 180 | }, 181 | }, 182 | ExpectedGroupsIDs: []string{"devs-group", "admins-group"}, 183 | DefaultGroupID: "", 184 | UserGroupMapping: UserGroupMapping, 185 | }, 186 | { 187 | TestName: "Receive many groups from idp with comma separated", 188 | CustomGroupIDField: "my-custom-group-id-field", 189 | user: goth.User{ 190 | RawData: map[string]interface{}{ 191 | "my-custom-group-id-field": "devs,admins", 192 | }, 193 | }, 194 | ExpectedGroupsIDs: []string{"devs-group", "admins-group"}, 195 | DefaultGroupID: "", 196 | UserGroupMapping: UserGroupMapping, 197 | UserGroupSeparator: ",", 198 | }, 199 | { 200 | TestName: "Custom group id field not empty & valid. With default group set", 201 | CustomGroupIDField: "my-custom-group-id-field", 202 | user: goth.User{ 203 | RawData: map[string]interface{}{ 204 | "my-custom-group-id-field": "admins", 205 | }, 206 | }, 207 | ExpectedGroupsIDs: []string{"admins-group"}, 208 | DefaultGroupID: "devs", 209 | UserGroupMapping: UserGroupMapping, 210 | }, 211 | { 212 | TestName: "Custom group id field not empty, and the claim being an array", 213 | CustomGroupIDField: "memberOf", 214 | user: goth.User{RawData: map[string]interface{}{ 215 | "memberOf": []string{ 216 | "CN=tyk_admin,OU=Security Groups,OU=GenericOrg,DC=GenericOrg,DC=COM,DC=GEN", 217 | "CN=openshift-uat-users,OU=Security Groups,OU=GenericOrg,DC=GenericOrg,DC=COM,DC=GEN", 218 | "CN=Generic Contract Employees,OU=Email_Group,OU=GenericOrg,DC=GenericOrg,DC=COM,DC=GEN", 219 | "CN=VPN-Group-Outsourced,OU=Security Groups,OU=GenericOrg,DC=GenericOrg,DC=COM,DC=GEN", 220 | "CN=Normal Group,OU=Security Groups,OU=GenericOrg,DC=GenericOrg,DC=COM,DC=GEN", 221 | }, 222 | }}, 223 | ExpectedGroupsIDs: []string{"tyk-admin"}, 224 | DefaultGroupID: "devs", 225 | UserGroupMapping: UserGroupMapping, 226 | }, 227 | } 228 | 229 | for _, tc := range cases { 230 | t.Run(tc.TestName, func(t *testing.T) { 231 | ids := GetGroupId(tc.user, tc.CustomGroupIDField, tc.DefaultGroupID, tc.UserGroupMapping, tc.UserGroupSeparator) 232 | assert.Equal(t, tc.ExpectedGroupsIDs, ids) 233 | }) 234 | } 235 | } 236 | 237 | func Test_defaultOrEmptyGroupIDs(t *testing.T) { 238 | tests := []struct { 239 | name string 240 | defaultUserGroup string 241 | expectedGroupIDs []string 242 | }{ 243 | { 244 | name: "Empty default user group", 245 | defaultUserGroup: "", 246 | expectedGroupIDs: []string{}, 247 | }, 248 | { 249 | name: "Non-empty default user group", 250 | defaultUserGroup: "defaultGroup", 251 | expectedGroupIDs: []string{"defaultGroup"}, 252 | }, 253 | { 254 | name: "Default user group with spaces", 255 | defaultUserGroup: "default group", 256 | expectedGroupIDs: []string{"default group"}, 257 | }, 258 | { 259 | name: "Default user group with special characters", 260 | defaultUserGroup: "group@123", 261 | expectedGroupIDs: []string{"group@123"}, 262 | }, 263 | } 264 | 265 | for _, tt := range tests { 266 | t.Run(tt.name, func(t *testing.T) { 267 | result := defaultOrEmptyGroupIDs(tt.defaultUserGroup) 268 | assert.Equal(t, tt.expectedGroupIDs, result, "The group IDs should match") 269 | }) 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /providers/reverse_proxy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // HTTP reverse proxy handler 6 | 7 | package providers 8 | 9 | import ( 10 | "io" 11 | "net" 12 | "net/http" 13 | "net/url" 14 | "strings" 15 | "sync" 16 | "time" 17 | 18 | logger "github.com/TykTechnologies/tyk-identity-broker/log" 19 | "github.com/sirupsen/logrus" 20 | ) 21 | 22 | var onceReloadReverseProxyLogger sync.Once 23 | var reverseProxyLogTag = "Reverse proxy" 24 | var reverseProxyLogger = log.WithField("prefix", reverseProxyLogTag) 25 | 26 | // onExitFlushLoop is a callback set by tests to detect the state of the 27 | // flushLoop() goroutine. 28 | var onExitFlushLoop func() 29 | 30 | // ReverseProxy is an HTTP Handler that takes an incoming request and 31 | // sends it to another server, proxying the response back to the 32 | // client. 33 | type ReverseProxy struct { 34 | // Director must be a function which modifies 35 | // the request into a new request to be sent 36 | // using Transport. Its response is then copied 37 | // back to the original client unmodified. 38 | Director func(*http.Request) 39 | 40 | // The transport used to perform proxy requests. 41 | // If nil, http.DefaultTransport is used. 42 | Transport http.RoundTripper 43 | 44 | // FlushInterval specifies the flush interval 45 | // to flush to the client while copying the 46 | // response body. 47 | // If zero, no periodic flushing is done. 48 | FlushInterval time.Duration 49 | } 50 | 51 | func singleJoiningSlash(a, b string) string { 52 | aslash := strings.HasSuffix(a, "/") 53 | bslash := strings.HasPrefix(b, "/") 54 | 55 | switch { 56 | case aslash && bslash: 57 | return a + b[1:] 58 | case !aslash && !bslash: 59 | if len(b) > 0 { 60 | return a + "/" + b 61 | } else { 62 | return a 63 | } 64 | 65 | } 66 | return a + b 67 | } 68 | 69 | // NewSingleHostReverseProxy returns a new ReverseProxy that rewrites 70 | // URLs to the scheme, host, and base path provided in target. If the 71 | // target's path is "/base" and the incoming request was for "/dir", 72 | // the target request will be for /base/dir. 73 | func NewSingleHostReverseProxy(target *url.URL) *ReverseProxy { 74 | targetQuery := target.RawQuery 75 | director := func(req *http.Request) { 76 | req.URL.Scheme = target.Scheme 77 | req.URL.Host = target.Host 78 | req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path) 79 | if targetQuery == "" || req.URL.RawQuery == "" { 80 | req.URL.RawQuery = targetQuery + req.URL.RawQuery 81 | } else { 82 | req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery 83 | } 84 | } 85 | return &ReverseProxy{Director: director} 86 | } 87 | 88 | func copyHeader(dst, src http.Header) { 89 | for k, vv := range src { 90 | for _, v := range vv { 91 | dst.Add(k, v) 92 | } 93 | } 94 | } 95 | 96 | // Hop-by-hop headers. These are removed when sent to the backend. 97 | // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html 98 | var hopHeaders = []string{ 99 | "Connection", 100 | "Keep-Alive", 101 | "Proxy-Authenticate", 102 | "Proxy-Authorization", 103 | "Te", // canonicalized version of "TE" 104 | "Trailers", 105 | "Transfer-Encoding", 106 | "Upgrade", 107 | } 108 | 109 | type requestCanceler interface { 110 | CancelRequest(*http.Request) 111 | } 112 | 113 | type runOnFirstRead struct { 114 | io.Reader // optional; nil means empty body 115 | 116 | fn func() // Run before first Read, then set to nil 117 | } 118 | 119 | func (c *runOnFirstRead) Read(bs []byte) (int, error) { 120 | if c.fn != nil { 121 | c.fn() 122 | c.fn = nil 123 | } 124 | if c.Reader == nil { 125 | return 0, io.EOF 126 | } 127 | return c.Reader.Read(bs) 128 | } 129 | 130 | func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 131 | //if an external logger was set, then lets reload it to inherit those configs 132 | onceReloadReverseProxyLogger.Do(func() { 133 | log = logger.Get() 134 | reverseProxyLogger = &logrus.Entry{Logger: log} 135 | reverseProxyLogger = reverseProxyLogger.Logger.WithField("prefix", reverseProxyLogTag) 136 | }) 137 | 138 | transport := p.Transport 139 | if transport == nil { 140 | transport = http.DefaultTransport 141 | } 142 | 143 | outreq := new(http.Request) 144 | *outreq = *req // includes shallow copies of maps, but okay 145 | 146 | if closeNotifier, ok := rw.(http.CloseNotifier); ok { 147 | if requestCanceler, ok := transport.(requestCanceler); ok { 148 | reqDone := make(chan struct{}) 149 | defer close(reqDone) 150 | 151 | clientGone := closeNotifier.CloseNotify() 152 | 153 | outreq.Body = struct { 154 | io.Reader 155 | io.Closer 156 | }{ 157 | Reader: &runOnFirstRead{ 158 | Reader: outreq.Body, 159 | fn: func() { 160 | go func() { 161 | select { 162 | case <-clientGone: 163 | requestCanceler.CancelRequest(outreq) 164 | case <-reqDone: 165 | } 166 | }() 167 | }, 168 | }, 169 | Closer: outreq.Body, 170 | } 171 | } 172 | } 173 | 174 | p.Director(outreq) 175 | outreq.Proto = "HTTP/1.1" 176 | outreq.ProtoMajor = 1 177 | outreq.ProtoMinor = 1 178 | outreq.Close = false 179 | 180 | // Remove hop-by-hop headers to the backend. Especially 181 | // important is "Connection" because we want a persistent 182 | // connection, regardless of what the client sent to us. This 183 | // is modifying the same underlying map from req (shallow 184 | // copied above) so we only copy it if necessary. 185 | copiedHeaders := false 186 | for _, h := range hopHeaders { 187 | if outreq.Header.Get(h) != "" { 188 | if !copiedHeaders { 189 | outreq.Header = make(http.Header) 190 | copyHeader(outreq.Header, req.Header) 191 | copiedHeaders = true 192 | } 193 | outreq.Header.Del(h) 194 | } 195 | } 196 | 197 | if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { 198 | // If we aren't the first proxy retain prior 199 | // X-Forwarded-For information as a comma+space 200 | // separated list and fold multiple headers into one. 201 | if prior, ok := outreq.Header["X-Forwarded-For"]; ok { 202 | clientIP = strings.Join(prior, ", ") + ", " + clientIP 203 | } 204 | outreq.Header.Set("X-Forwarded-For", clientIP) 205 | } 206 | 207 | res, err := transport.RoundTrip(outreq) 208 | if err != nil { 209 | reverseProxyLogger.Error(err) 210 | rw.WriteHeader(http.StatusInternalServerError) 211 | return 212 | } 213 | 214 | for _, h := range hopHeaders { 215 | res.Header.Del(h) 216 | } 217 | 218 | copyHeader(rw.Header(), res.Header) 219 | 220 | // The "Trailer" header isn't included in the Transport's response, 221 | // at least for *http.Transport. Build it up from Trailer. 222 | if len(res.Trailer) > 0 { 223 | var trailerKeys []string 224 | for k := range res.Trailer { 225 | trailerKeys = append(trailerKeys, k) 226 | } 227 | rw.Header().Add("Trailer", strings.Join(trailerKeys, ", ")) 228 | } 229 | 230 | rw.WriteHeader(res.StatusCode) 231 | if len(res.Trailer) > 0 { 232 | // Force chunking if we saw a response trailer. 233 | // This prevents net/http from calculating the length for short 234 | // bodies and adding a Content-Length. 235 | if fl, ok := rw.(http.Flusher); ok { 236 | fl.Flush() 237 | } 238 | } 239 | p.copyResponse(rw, res.Body) 240 | res.Body.Close() // close now, instead of defer, to populate res.Trailer 241 | copyHeader(rw.Header(), res.Trailer) 242 | } 243 | 244 | func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader) { 245 | if p.FlushInterval != 0 { 246 | if wf, ok := dst.(writeFlusher); ok { 247 | mlw := &maxLatencyWriter{ 248 | dst: wf, 249 | latency: p.FlushInterval, 250 | done: make(chan bool), 251 | } 252 | go mlw.flushLoop() 253 | defer mlw.stop() 254 | dst = mlw 255 | } 256 | } 257 | 258 | io.Copy(dst, src) 259 | } 260 | 261 | func (p *ReverseProxy) logf(format string, args ...interface{}) { 262 | log.Printf(format, args...) 263 | 264 | } 265 | 266 | type writeFlusher interface { 267 | io.Writer 268 | http.Flusher 269 | } 270 | 271 | type maxLatencyWriter struct { 272 | dst writeFlusher 273 | latency time.Duration 274 | 275 | lk sync.Mutex // protects Write + Flush 276 | done chan bool 277 | } 278 | 279 | func (m *maxLatencyWriter) Write(p []byte) (int, error) { 280 | m.lk.Lock() 281 | defer m.lk.Unlock() 282 | return m.dst.Write(p) 283 | } 284 | 285 | func (m *maxLatencyWriter) flushLoop() { 286 | t := time.NewTicker(m.latency) 287 | defer t.Stop() 288 | for { 289 | select { 290 | case <-m.done: 291 | if onExitFlushLoop != nil { 292 | onExitFlushLoop() 293 | } 294 | return 295 | case <-t.C: 296 | m.lk.Lock() 297 | m.dst.Flush() 298 | m.lk.Unlock() 299 | } 300 | } 301 | } 302 | 303 | func (m *maxLatencyWriter) stop() { m.done <- true } 304 | -------------------------------------------------------------------------------- /tothic/tothic.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package Tothic wraps Gothic behaviour for multi-tenant usage. Package gothic wraps common behaviour when using Goth. This makes it quick, and easy, to get up 3 | and running with Goth. Of course, if you want complete control over how things flow, in regards 4 | to the authentication process, feel free and use Goth directly. 5 | 6 | See https://github.com/markbates/goth/examples/main.go to see this in action. 7 | */ 8 | package tothic 9 | 10 | import ( 11 | "encoding/json" 12 | "errors" 13 | "net/http" 14 | "os" 15 | "strings" 16 | 17 | "github.com/TykTechnologies/tyk-identity-broker/internal/jwe" 18 | "github.com/markbates/goth/providers/openidConnect" 19 | 20 | logger "github.com/TykTechnologies/tyk-identity-broker/log" 21 | "github.com/TykTechnologies/tyk-identity-broker/tap" 22 | "github.com/TykTechnologies/tyk-identity-broker/toth" 23 | "github.com/gorilla/sessions" 24 | "github.com/markbates/goth" 25 | ) 26 | 27 | // SessionName is the key used to access the session store. 28 | const SessionName = "_gothic_session" 29 | 30 | const EnvPrefix = "TYK_IB" 31 | 32 | var log = logger.Get() 33 | 34 | // var pathParams map[string]string 35 | var pathParams tap.AuthRegisterBackend 36 | 37 | var TothErrorHandler func(string, string, error, int, http.ResponseWriter, *http.Request) 38 | 39 | // Store can/should be set by applications using gothic. The default is a cookie store. 40 | var Store sessions.Store 41 | 42 | type PathParam struct { 43 | Id string `json:"id"` 44 | Provider string `json:"provider"` 45 | } 46 | 47 | func (p PathParam) UnmarshalBinary(data []byte) error { 48 | // convert data to yours, let's assume its json data 49 | return json.Unmarshal(data, p) 50 | } 51 | 52 | func (p PathParam) MarshalBinary() ([]byte, error) { 53 | return json.Marshal(p) 54 | } 55 | 56 | func SetupSessionStore() { 57 | key := KeyFromEnv() 58 | Store = sessions.NewCookieStore([]byte(key)) 59 | } 60 | 61 | func KeyFromEnv() (key string) { 62 | // To handle deprecation 63 | key = os.Getenv("SESSION_SECRET") 64 | temp := os.Getenv(EnvPrefix + "_SESSION_SECRET") 65 | if temp != "" { 66 | if key != "" { 67 | log.Warn("SESSION_SECRET is deprecated, TYK_IB_SESSION_SECRET overrides it when you set both.") 68 | } 69 | key = temp 70 | } 71 | 72 | if key == "" && temp == "" { 73 | log.Warn("toth/tothic: no TYK_IB_SESSION_SECRET environment variable is set. The default cookie store is not available and any calls will fail. Ignore this warning if you are using a different store.") 74 | } 75 | 76 | return 77 | } 78 | 79 | func SetPathParams(newPathParams map[string]string, profile tap.Profile) { 80 | 81 | val, ok := newPathParams[":provider"] 82 | if ok { 83 | newPathParams["provider"] = val 84 | delete(newPathParams, ":provider") 85 | } 86 | 87 | jsonbody, err := json.Marshal(newPathParams) 88 | if err != nil { 89 | log.WithError(err).Error("saving path params") 90 | return 91 | } 92 | 93 | params := PathParam{} 94 | if err := json.Unmarshal(jsonbody, ¶ms); err != nil { 95 | log.WithError(err).Error("saving path params") 96 | return 97 | } 98 | 99 | err = pathParams.SetKey(profile.GetPrefix(), profile.OrgID, params) 100 | if err != nil { 101 | log.WithError(err).Error("saving path params") 102 | } 103 | 104 | } 105 | 106 | func GetParams(profile tap.Profile) PathParam { 107 | params := PathParam{} 108 | pathParams.GetKey(profile.GetPrefix(), profile.OrgID, ¶ms) 109 | return params 110 | } 111 | 112 | /* 113 | BeginAuthHandler is a convienence handler for starting the authentication process. 114 | It expects to be able to get the name of the provider from the query parameters 115 | as either "provider" or ":provider". 116 | 117 | BeginAuthHandler will redirect the user to the appropriate authentication end-point 118 | for the requested provider. 119 | 120 | See https://github.com/markbates/goth/examples/main.go to see this in action. 121 | */ 122 | func BeginAuthHandler(res http.ResponseWriter, req *http.Request, toth *toth.TothInstance, pathParams map[string]string, profile tap.Profile) { 123 | 124 | SetPathParams(pathParams, profile) 125 | 126 | url, err := GetAuthURL(res, req, toth, profile) 127 | if err != nil { 128 | //res.WriteHeader(http.StatusBadRequest) 129 | //fmt.Fprintln(res, err) 130 | TothErrorHandler("[TOTHIC]", err.Error(), err, http.StatusBadRequest, res, req) 131 | return 132 | } 133 | 134 | http.Redirect(res, req, url, http.StatusTemporaryRedirect) 135 | } 136 | 137 | // GetState gets the state returned by the provider during the callback. 138 | // This is used to prevent CSRF attacks, see 139 | // http://tools.ietf.org/html/rfc6749#section-10.12 140 | var GetState = func(req *http.Request) string { 141 | params := req.URL.Query() 142 | 143 | state := params.Get("state") 144 | 145 | if state == "" && req.Method == http.MethodPost { 146 | state = req.FormValue("state") 147 | } 148 | 149 | if state == "" { 150 | // no "state" found, returning the default value 151 | state = "state" 152 | } 153 | 154 | return state 155 | } 156 | 157 | /* 158 | GetAuthURL starts the authentication process with the requested provided. 159 | It will return a URL that should be used to send users to. 160 | 161 | It expects to be able to get the name of the provider from the query parameters 162 | as either "provider" or ":provider". 163 | 164 | I would recommend using the BeginAuthHandler instead of doing all of these steps 165 | yourself, but that's entirely up to you. 166 | */ 167 | func GetAuthURL(res http.ResponseWriter, req *http.Request, toth *toth.TothInstance, profile tap.Profile) (string, error) { 168 | 169 | providerName, err := GetProviderName(profile) 170 | if err != nil { 171 | return "", err 172 | } 173 | 174 | provider, err := toth.GetProvider(providerName) 175 | if err != nil { 176 | return "", err 177 | } 178 | sess, err := provider.BeginAuth(GetState(req)) 179 | if err != nil { 180 | return "", err 181 | } 182 | 183 | url, err := sess.GetAuthURL() 184 | if err != nil { 185 | return "", err 186 | } 187 | 188 | session, _ := Store.Get(req, SessionName) 189 | session.Values[SessionName] = sess.Marshal() 190 | err = session.Save(req, res) 191 | if err != nil { 192 | return "", err 193 | } 194 | 195 | return url, err 196 | } 197 | 198 | /* 199 | CompleteUserAuth does what it says on the tin. It completes the authentication 200 | process and fetches all of the basic information about the user from the provider. 201 | 202 | It expects to be able to get the name of the provider from the query parameters 203 | as either "provider" or ":provider". 204 | 205 | See https://github.com/markbates/goth/examples/main.go to see this in action. 206 | */ 207 | var CompleteUserAuth = func(res http.ResponseWriter, req *http.Request, toth *toth.TothInstance, profile tap.Profile, jweHandler *jwe.Handler) (goth.User, error) { 208 | providerName, err := GetProviderName(profile) 209 | if err != nil { 210 | return goth.User{}, err 211 | } 212 | 213 | provider, err := toth.GetProvider(providerName) 214 | if err != nil { 215 | return goth.User{}, err 216 | } 217 | 218 | session, err := Store.Get(req, SessionName) 219 | if err != nil { 220 | return goth.User{}, errors.New("cannot get session store") 221 | } 222 | 223 | if session.Values[SessionName] == nil { 224 | return goth.User{}, errors.New("could not find a matching session for this request") 225 | } 226 | 227 | sess, err := provider.UnmarshalSession(session.Values[SessionName].(string)) 228 | if err != nil { 229 | return goth.User{}, err 230 | } 231 | 232 | _, err = sess.Authorize(provider, req.URL.Query()) 233 | if err != nil { 234 | return goth.User{}, err 235 | } 236 | 237 | JWTSession, err := prepareJWTSession(sess, jweHandler) 238 | if err != nil { 239 | return goth.User{}, err 240 | } 241 | 242 | return provider.FetchUser(JWTSession) 243 | } 244 | 245 | func prepareJWTSession(sess goth.Session, jweHandler *jwe.Handler) (goth.Session, error) { 246 | // no decryption is required 247 | if !jweHandler.Enabled { 248 | return sess, nil 249 | } 250 | 251 | var err error 252 | JWTSession := &openidConnect.Session{} 253 | err = json.NewDecoder(strings.NewReader(sess.Marshal())).Decode(JWTSession) 254 | if err != nil { 255 | return nil, err 256 | } 257 | 258 | // we must decrypt the ID token 259 | err = jwe.DecryptIDToken(jweHandler, JWTSession) 260 | if err != nil { 261 | return nil, err 262 | } 263 | return JWTSession, nil 264 | } 265 | 266 | // GetProviderName is a function used to get the name of a provider 267 | // for a given request. By default, this provider is fetched from 268 | // the URL query string. If you provide it in a different way, 269 | // assign your own function to this variable that returns the provider 270 | // name for your request. 271 | var GetProviderName = getProviderName 272 | 273 | func getProviderName(profile tap.Profile) (string, error) { 274 | params := GetParams(profile) 275 | 276 | provider := params.Provider 277 | if provider == "" { 278 | return provider, errors.New("you must select a provider") 279 | } 280 | 281 | log.Info("Provider:", provider) 282 | return provider, nil 283 | } 284 | 285 | func SetParamsStoreHandler(newParamsStore tap.AuthRegisterBackend) { 286 | pathParams = newParamsStore 287 | } 288 | --------------------------------------------------------------------------------