Compare commits

...

No commits in common. "master" and "v1" have entirely different histories.
master ... v1

1996 changed files with 835300 additions and 88472 deletions

View File

@ -1,4 +0,0 @@
.github/
.gitpod.yml
bin/
tmp/

View File

@ -1,21 +0,0 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.go]
indent_style = tab
[*.proto]
indent_size = 2
[{Makefile,*.mk}]
indent_style = tab
[{config.yaml.dist,config.dev.yaml}]
indent_size = 2

6
.envrc
View File

@ -1,6 +0,0 @@
if ! has nix_direnv_version || ! nix_direnv_version 1.5.0; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/1.5.0/direnvrc" "sha256-carKk9aUFHMuHt+IWh74hFj58nY4K3uywpZbwXX0BTI="
fi
use flake
dotenv_if_exists

View File

@ -1,2 +0,0 @@
[{*.yml,*.yaml}]
indent_size = 2

View File

@ -1,3 +0,0 @@
## Community Code of Conduct
This project follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).

View File

@ -1,102 +0,0 @@
name: 🐛 Bug report
description: Report a bug to help us improve Dex
body:
- type: markdown
attributes:
value: |
Thank you for submitting a bug report!
Please fill out the template below to make it easier to debug your problem.
If you are not sure if it is a bug or not, you can contact us via the available [support channels](https://github.com/dexidp/dex/issues/new/choose).
- type: checkboxes
attributes:
label: Preflight Checklist
description: Please ensure you've completed all of the following.
options:
- label: I agree to follow the [Code of Conduct](https://github.com/dexidp/dex/blob/master/.github/CODE_OF_CONDUCT.md) that this project adheres to.
required: true
- label: I have searched the [issue tracker](https://www.github.com/dexidp/dex/issues) for an issue that matches the one I want to file, without success.
required: true
- label: I am not looking for support or already pursued the available [support channels](https://github.com/dexidp/dex/issues/new/choose) without success.
required: true
- type: input
attributes:
label: Version
description: What version of Dex are you running?
placeholder: 2.29.0
validations:
required: true
- type: dropdown
attributes:
label: Storage Type
description: Which persistent storage type are you using?
options:
- etcd
- Kubernetes
- In-memory
- Postgres
- MySQL
- SQLite
validations:
required: true
- type: dropdown
attributes:
label: Installation Type
description: How did you install Dex?
options:
- Binary
- Official container image
- Official Helm chart
- Custom container image
- Custom Helm chart
- Other (specify below)
multiple: true
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Actual Behavior
description: A clear description of what actually happens.
validations:
required: true
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior if it is not self-explanatory.
placeholder: |
1. In this environment...
2. With this config...
3. Run '...'
4. See error...
- type: textarea
attributes:
label: Additional Information
description: Links? References? Anything that will give us more context about the issue that you are encountering!
- type: textarea
attributes:
label: Configuration
description: Contents of your configuration file (if relevant).
render: yaml
placeholder: |
issuer: http://127.0.0.1:5556/dex
storage:
# ...
connectors:
# ...
staticClients:
# ...
- type: textarea
attributes:
label: Logs
description: Dex application logs (if relevant).
render: shell

View File

@ -1,17 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: ❓ Ask a question
url: https://github.com/dexidp/dex/discussions/new?category=q-a
about: Ask and discuss questions with other Dex community members
- name: 📚 Documentation
url: https://dexidp.io/docs/
about: Check the documentation for help
- name: 💬 Slack channel
url: https://cloud-native.slack.com/messages/dexidp
about: Please ask and answer questions here
- name: 💡 Dex Enhancement Proposal
url: https://github.com/dexidp/dex/tree/master/enhancements/README.md
about: Open a proposal for significant architectural change

View File

@ -1,40 +0,0 @@
name: 🎉 Feature request
description: Suggest an idea for Dex
body:
- type: markdown
attributes:
value: |
Thank you for submitting a feature request!
Please describe what you would like to change/add and why in detail by filling out the template below.
If you are not sure if your request fits into Dex, you can contact us via the available [support channels](https://github.com/dexidp/dex/issues/new/choose).
- type: checkboxes
attributes:
label: Preflight Checklist
description: Please ensure you've completed all of the following.
options:
- label: I agree to follow the [Code of Conduct](https://github.com/dexidp/dex/blob/master/.github/CODE_OF_CONDUCT.md) that this project adheres to.
required: true
- label: I have searched the [issue tracker](https://www.github.com/dexidp/dex/issues) for an issue that matches the one I want to file, without success.
required: true
- type: textarea
attributes:
label: Problem Description
description: A clear and concise description of the problem you are seeking to solve with this feature request.
validations:
required: true
- type: textarea
attributes:
label: Proposed Solution
description: A clear and concise description of what would you like to happen.
validations:
required: true
- type: textarea
attributes:
label: Alternatives Considered
description: A clear and concise description of any alternative solutions or features you've considered.
- type: textarea
attributes:
label: Additional Information
description: Add any other context about the problem here.

View File

@ -1,35 +0,0 @@
<!--
Thank you for sending a pull request! Here are some tips for contributors:
1. Fill the description template below.
2. Sign a DCO (if you haven't already signed it).
3. Include appropriate tests (if necessary). Make sure that all CI checks passed.
4. If the Pull Request is a work in progress, make use of GitHub's "Draft PR" feature and mark it as such.
-->
#### Overview
<!-- Describe your changes briefly here. -->
#### What this PR does / why we need it
<!--
- Please state in detail why we need this PR and what it solves.
- If your PR closes some of the existing issues, please add links to them here.
Mentioned issues will be automatically closed.
Usage: "Closes #<issue number>", or "Closes (paste link of issue)"
-->
#### Special notes for your reviewer
#### Does this PR introduce a user-facing change?
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".
-->
```release-note
```

24
.github/SECURITY.md vendored
View File

@ -1,24 +0,0 @@
# Security Policy
## Reporting a vulnerability
To report a vulnerability, send an email to [cncf-dex-maintainers@lists.cncf.io](mailto:cncf-dex-maintainers@lists.cncf.io)
detailing the issue and steps to reproduce. The reporter(s) can expect a
response within 48 hours acknowledging the issue was received. If a response is
not received within 48 hours, please reach out to any maintainer directly
to confirm receipt of the issue.
## Review Process
Once a maintainer has confirmed the relevance of the report, a draft security
advisory will be created on Github. The draft advisory will be used to discuss
the issue with maintainers, the reporter(s).
If the reporter(s) wishes to participate in this discussion, then provide
reporter Github username(s) to be invited to the discussion. If the reporter(s)
does not wish to participate directly in the discussion, then the reporter(s)
can request to be updated regularly via email.
If the vulnerability is accepted, a timeline for developing a patch, public
disclosure, and patch release will be determined. The reporter(s) are expected
to participate in the discussion of the timeline and abide by agreed upon dates
for public disclosure.

View File

@ -1,30 +0,0 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
labels:
- "area/dependencies"
schedule:
interval: "daily"
- package-ecosystem: "gomod"
directory: "/api/v2"
labels:
- "area/dependencies"
schedule:
interval: "daily"
- package-ecosystem: "docker"
directory: "/"
labels:
- "area/dependencies"
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
labels:
- "area/dependencies"
schedule:
interval: "daily"

30
.github/release.yml vendored
View File

@ -1,30 +0,0 @@
changelog:
exclude:
labels:
- release-note/ignore
categories:
- title: Exciting New Features 🎉
labels:
- kind/feature
- release-note/new-feature
- title: Enhancements 🚀
labels:
- kind/enhancement
- release-note/enhancement
- title: Bug Fixes 🐛
labels:
- kind/bug
- release-note/bug-fix
- title: Breaking Changes 🛠
labels:
- release-note/breaking-change
- title: Deprecations ❌
labels:
- release-note/deprecation
- title: Dependency Updates ⬆️
labels:
- area/dependencies
- release-note/dependency-update
- title: Other Changes
labels:
- "*"

View File

@ -1,97 +0,0 @@
name: Artifacts
on:
push:
branches:
- master
tags:
- v[0-9]+.[0-9]+.[0-9]+
pull_request:
jobs:
container-images:
name: Container images
runs-on: ubuntu-latest
strategy:
matrix:
variant:
- alpine
- distroless
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Gather metadata
id: meta
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/dexidp/dex
dexidp/dex
flavor: |
latest = false
tags: |
type=ref,event=branch,enable=${{ matrix.variant == 'alpine' }}
type=ref,event=pr,enable=${{ matrix.variant == 'alpine' }}
type=semver,pattern={{raw}},enable=${{ matrix.variant == 'alpine' }}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && matrix.variant == 'alpine' }}
type=ref,event=branch,suffix=-${{ matrix.variant }}
type=ref,event=pr,suffix=-${{ matrix.variant }}
type=semver,pattern={{raw}},suffix=-${{ matrix.variant }}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }},suffix=-${{ matrix.variant }}
labels: |
org.opencontainers.image.documentation=https://dexidp.io/docs/
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
platforms: all
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ github.token }}
if: github.event_name == 'push'
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
if: github.event_name == 'push'
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le
# cache-from: type=gha
# cache-to: type=gha,mode=max
push: ${{ github.event_name == 'push' }}
tags: ${{ steps.meta.outputs.tags }}
build-args: |
BASE_IMAGE=${{ matrix.variant }}
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
COMMIT_HASH=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}
labels: ${{ steps.meta.outputs.labels }}
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.6.1
with:
image-ref: "ghcr.io/dexidp/dex:${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}"
format: "sarif"
output: "trivy-results.sarif"
if: github.event_name == 'push'
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: "trivy-results.sarif"
if: github.event_name == 'push'

View File

@ -1,18 +0,0 @@
name: PR Checks
on:
pull_request:
types: [opened, labeled, unlabeled, synchronize]
jobs:
release-label:
name: Release note label
runs-on: ubuntu-latest
steps:
- name: Check minimum labels
uses: mheap/github-action-required-labels@v2
with:
mode: minimum
count: 1
labels: "release-note/ignore, kind/feature, release-note/new-feature, kind/enhancement, release-note/enhancement, kind/bug, release-note/bug-fix, release-note/breaking-change, release-note/deprecation, area/dependencies, release-note/dependency-update"

View File

@ -1,129 +0,0 @@
name: CI
on:
push:
branches:
- master
pull_request:
jobs:
build:
name: Build
runs-on: ubuntu-latest
env:
GOFLAGS: -mod=readonly
services:
postgres:
image: postgres:10.8
ports:
- 5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
postgres-ent:
image: postgres:10.8
ports:
- 5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
mysql:
image: mysql:5.7
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: dex
ports:
- 3306
options: --health-cmd "mysql -proot -e \"show databases;\"" --health-interval 10s --health-timeout 5s --health-retries 5
mysql-ent:
image: mysql:5.7
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: dex
ports:
- 3306
options: --health-cmd "mysql -proot -e \"show databases;\"" --health-interval 10s --health-timeout 5s --health-retries 5
etcd:
image: gcr.io/etcd-development/etcd:v3.5.0
ports:
- 2379
env:
ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379
ETCD_ADVERTISE_CLIENT_URLS: http://0.0.0.0:2379
options: --health-cmd "ETCDCTL_API=3 etcdctl --endpoints http://localhost:2379 endpoint health" --health-interval 10s --health-timeout 5s --health-retries 5
keystone:
image: openio/openstack-keystone:rocky
ports:
- 5000
- 35357
options: --health-cmd "curl --fail http://localhost:5000/v3" --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18
- name: Checkout code
uses: actions/checkout@v3
- name: Start services
run: docker-compose -f docker-compose.test.yaml up -d
- name: Create kind cluster
uses: helm/kind-action@v1.3.0
with:
version: v0.11.1
node_image: kindest/node:v1.19.11@sha256:07db187ae84b4b7de440a73886f008cf903fcf5764ba8106a9fd5243d6f32729
- name: Download tool dependencies
run: make deps
- name: Test
run: make testall
env:
DEX_MYSQL_DATABASE: dex
DEX_MYSQL_USER: root
DEX_MYSQL_PASSWORD: root
DEX_MYSQL_HOST: 127.0.0.1
DEX_MYSQL_PORT: ${{ job.services.mysql.ports[3306] }}
DEX_MYSQL_ENT_DATABASE: dex
DEX_MYSQL_ENT_USER: root
DEX_MYSQL_ENT_PASSWORD: root
DEX_MYSQL_ENT_HOST: 127.0.0.1
DEX_MYSQL_ENT_PORT: ${{ job.services.mysql-ent.ports[3306] }}
DEX_POSTGRES_DATABASE: postgres
DEX_POSTGRES_USER: postgres
DEX_POSTGRES_PASSWORD: postgres
DEX_POSTGRES_HOST: localhost
DEX_POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }}
DEX_POSTGRES_ENT_DATABASE: postgres
DEX_POSTGRES_ENT_USER: postgres
DEX_POSTGRES_ENT_PASSWORD: postgres
DEX_POSTGRES_ENT_HOST: localhost
DEX_POSTGRES_ENT_PORT: ${{ job.services.postgres-ent.ports[5432] }}
DEX_ETCD_ENDPOINTS: http://localhost:${{ job.services.etcd.ports[2379] }}
DEX_LDAP_HOST: localhost
DEX_LDAP_PORT: 389
DEX_LDAP_TLS_PORT: 636
DEX_KEYSTONE_URL: http://localhost:${{ job.services.keystone.ports[5000] }}
DEX_KEYSTONE_ADMIN_URL: http://localhost:${{ job.services.keystone.ports[35357] }}
DEX_KEYSTONE_ADMIN_USER: demo
DEX_KEYSTONE_ADMIN_PASS: DEMO_PASS
DEX_KUBERNETES_CONFIG_PATH: ~/.kube/config
- name: Lint
run: make lint
# Ensure proto generation doesn't depend on external packages.
- name: Verify proto
run: make verify-proto

View File

@ -1,67 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master, v1 ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '28 10 * * 6'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View File

@ -1,111 +0,0 @@
name: Docker
on:
# push:
# branches:
# - master
# tags:
# - v[0-9]+.[0-9]+.[0-9]+
pull_request:
jobs:
docker:
name: Docker
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Calculate Docker image tags
id: tags
env:
DOCKER_IMAGES: "ghcr.io/dexidp/dex dexidp/dex"
run: |
case $GITHUB_REF in
refs/tags/*) VERSION=${GITHUB_REF#refs/tags/};;
refs/heads/*) VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -r 's#/+#-#g');;
refs/pull/*) VERSION=pr-${{ github.event.number }};;
*) VERSION=sha-${GITHUB_SHA::8};;
esac
TAGS=()
for image in $DOCKER_IMAGES; do
TAGS+=("${image}:${VERSION}")
if [[ "${{ github.event.repository.default_branch }}" == "$VERSION" ]]; then
TAGS+=("${image}:latest")
fi
done
echo ::set-output name=version::${VERSION}
echo ::set-output name=tags::$(IFS=,; echo "${TAGS[*]}")
echo ::set-output name=commit_hash::${GITHUB_SHA::8}
echo ::set-output name=build_date::$(git show -s --format=%cI)
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
platforms: all
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
install: true
version: latest
# TODO: Remove driver-opts once fix is released docker/buildx#386
driver-opts: image=moby/buildkit:master
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ github.token }}
if: github.event_name == 'push'
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
if: github.event_name == 'push'
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le
# cache-from: type=gha
# cache-to: type=gha,mode=max
push: ${{ github.event_name == 'push' }}
tags: ${{ steps.tags.outputs.tags }}
build-args: |
VERSION=${{ steps.tags.outputs.version }}
COMMIT_HASH=${{ steps.tags.outputs.commit_hash }}
BUILD_DATE=${{ steps.tags.outputs.build_date }}
labels: |
org.opencontainers.image.title=${{ github.event.repository.name }}
org.opencontainers.image.description=${{ github.event.repository.description }}
org.opencontainers.image.url=${{ github.event.repository.html_url }}
org.opencontainers.image.source=${{ github.event.repository.clone_url }}
org.opencontainers.image.version=${{ steps.tags.outputs.version }}
org.opencontainers.image.created=${{ steps.tags.outputs.build_date }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.licenses=${{ github.event.repository.license.spdx_id }}
org.opencontainers.image.documentation=https://dexidp.io/docs/
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@0.6.1
with:
image-ref: "ghcr.io/dexidp/dex:${{ steps.tags.outputs.version }}"
format: "template"
template: "@/contrib/sarif.tpl"
output: "trivy-results.sarif"
if: github.event_name == 'push'
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: "trivy-results.sarif"
if: github.event_name == 'push'

36
.gitignore vendored
View File

@ -1,7 +1,29 @@
/.direnv/
/.idea/
/bin/
/config.yaml
/docker-compose.override.yaml
/var/
/vendor/
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
bin/
coverage/
deploy/
gopath/

View File

@ -1,3 +0,0 @@
tasks:
- init: go get && go build ./... && go test ./... && make
command: go run

View File

@ -1,90 +0,0 @@
run:
timeout: 4m
linters-settings:
depguard:
list-type: blacklist
include-go-root: true
packages:
- io/ioutil
packages-with-error-message:
- io/ioutil: "The 'io/ioutil' package is deprecated. Use corresponding 'os' or 'io' functions instead."
gci:
local-prefixes: github.com/dexidp/dex
goimports:
local-prefixes: github.com/dexidp/dex
linters:
disable-all: true
enable:
- bodyclose
- deadcode
- depguard
- dogsled
- exhaustive
- exportloopref
- gci
- gochecknoinits
- gocritic
- gofmt
- gofumpt
- goimports
- goprintffuncname
- gosimple
- govet
- ineffassign
- misspell
- nakedret
- nolintlint
- prealloc
- revive
- rowserrcheck
- sqlclosecheck
- staticcheck
- structcheck
- stylecheck
- tparallel
- unconvert
- unparam
- unused
- varcheck
- whitespace
# Disable temporarily until everything works with Go 1.18
# - typecheck
# TODO: fix linter errors before enabling
# - exhaustivestruct
# - gochecknoglobals
# - errorlint
# - gocognit
# - godot
# - nlreturn
# - noctx
# - wrapcheck
# TODO: fix linter errors before enabling (from original config)
# - dupl
# - errcheck
# - goconst
# - gocyclo
# - gosec
# - lll
# - scopelint
# unused
# - goheader
# - gomodguard
# don't enable:
# - asciicheck
# - funlen
# - godox
# - goerr113
# - gomnd
# - interfacer
# - maligned
# - nestif
# - testpackage
# - wsl

44
.travis.yml Normal file
View File

@ -0,0 +1,44 @@
sudo: required
services:
- docker
language: go
go:
- 1.5.4
- 1.6.2
- tip
env:
- DEX_TEST_DSN="postgres://postgres@127.0.0.1:15432/postgres?sslmode=disable" ISOLATED=true
DEX_TEST_LDAP_HOST="tlstest.local:1389"
DEX_TEST_LDAPS_HOST="tlstest.local:1636"
DEX_TEST_LDAP_BINDNAME="cn=admin,dc=example,dc=org"
DEX_TEST_LDAP_BINDPASS="admin"
install:
- go get golang.org/x/tools/cmd/cover
- docker pull quay.io/coreos/postgres
- docker pull osixia/openldap
script:
- docker run -d -p 127.0.0.1:15432:5432 quay.io/coreos/postgres
- LDAPCONTAINER=`docker run -e LDAP_TLS_PROTOCOL_MIN=3.0 -e LDAP_TLS_CIPHER_SUITE=NORMAL -d -p 127.0.0.1:1389:389 -p 127.0.0.1:1636:636 -h tlstest.local osixia/openldap:1.1.4`
- ./build
- ./test
- docker cp ${LDAPCONTAINER}:container/service/:cfssl/assets/default-ca/default-ca.pem /tmp/openldap-ca.pem
- docker cp ${LDAPCONTAINER}:container/service/slapd/assets/certs/ldap.key /tmp/ldap.key
- chmod 644 /tmp/ldap.key
- docker cp ${LDAPCONTAINER}:container/service/slapd/assets/certs/ldap.crt /tmp/ldap.crt
- sudo sh -c 'echo "127.0.0.1 tlstest.local" >> /etc/hosts'
- ./test-functional
- DEX_TEST_DSN="sqlite3://:memory:" ./test-functional
- ./build-docker
- ./build-docker clean
notifications:
email: false
matrix:
allow_failures:
- go: tip

View File

@ -1,15 +0,0 @@
# Adopters
This is a list of production adopters of Dex (in alphabetical order):
- [Aspect](https://www.aspect.com/) uses Dex for authenticating users across their Kubernetes infrastructure (using Kubernetes OIDC support).
- [Banzai Cloud](https://banzaicloud.com) is using Dex for authenticating to its Pipeline control plane and also to authenticate users against provisioned Kubernetes clusters (via Kubernetes OIDC support).
- [Chef](https://chef.io) uses Dex for authenticating users in [Chef Automate](https://automate.chef.io/). The code is Open Source, available at [`github.com/chef/automate`](https://github.com/chef/automate).
- [Elastisys](https://elastisys.com) uses Dex for authentication in their [Compliant Kubernetes](https://compliantkubernetes.io) distribution, including SSO to the custom dashboard, Grafana, Kibana, and Harbor.
- [Flant](https://flant.com) uses Dex for providing access to core components of [Managed Kubernetes as a Service](https://flant.com/services/managed-kubernetes-as-a-service), integration with various authentication providers, plugging custom applications.
- [JuliaBox](https://juliabox.com/) is leveraging federated OIDC provided by Dex for authenticating users to their compute infrastructure based on Kubernetes.
- [Kasten](https://www.kasten.io) is using Dex for authenticating access to the dashboard of [K10](https://www.kasten.io/product/), a Kubernetes-native platform for backup, disaster recovery and mobility of Kubernetes applications. K10 is widely used by a variety of customers including large enterprises, financial services, design firms, and IT companies.
- [Kyma](https://kyma-project.io) is using Dex to authenticate access to Kubernetes API server (even for managed Kubernetes like Google Kubernetes Engine or Azure Kubernetes Service) and for protecting web UI of [Kyma Console](https://github.com/kyma-project/console) and other UIs integrated in Kyma ([Grafana](https://github.com/grafana/grafana), [Loki](https://github.com/grafana/loki), and [Jaeger](https://github.com/jaegertracing/jaeger)). Kyma is an open-source project ([`github.com/kyma-project`](https://github.com/kyma-project/kyma)) designed natively on Kubernetes, that allows you to extend and customize your applications in a quick and modern way, using serverless computing or microservice architecture.
- [Pusher](https://pusher.com) uses Dex for authenticating users across their Kubernetes infrastructure (using Kubernetes OIDC support) in conjunction with the [OAuth2 Proxy](https://github.com/pusher/oauth2_proxy) for protecting web UIs.
- [Pydio](https://pydio.com/) Pydio Cells is an open source sync & share platform written in Go. Cells is using Dex as an OIDC service for authentication and authorizations. Check out [Pydio Cells repository](https://github.com/pydio/cells) for more information and/or to contribute.
- [sigstore](https://sigstore.dev) uses Dex for authentication in their public Fulcio instance, which is a certificate authority for code signing certificates bound to OIDC-based identities.

88
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,88 @@
# How to Contribute
CoreOS projects are [Apache 2.0 licensed](LICENSE) and accept contributions via
GitHub pull requests. This document outlines some of the conventions on
development workflow, commit message formatting, contact points and other
resources to make it easier to get your contribution accepted.
# Certificate of Origin
By contributing to this project you agree to the Developer Certificate of
Origin (DCO). This document was created by the Linux Kernel community and is a
simple statement that you, as a contributor, have the legal right to make the
contribution. See the [DCO](DCO) file for details.
# Email and Chat
The project currently uses the general CoreOS email list and IRC channel:
- Email: [coreos-dev](https://groups.google.com/forum/#!forum/coreos-dev)
- IRC: #[coreos](irc://irc.freenode.org:6667/#coreos) IRC channel on freenode.org
Please avoid emailing maintainers found in the MAINTAINERS file directly. They
are very busy and read the mailing lists.
## Getting Started
- Fork the repository on GitHub
- Read the [README](README.md) for build and test instructions
- Play with the project, submit bugs, submit patches!
## Contribution Flow
This is a rough outline of what a contributor's workflow looks like:
- Create a proposal (if neccessary - see below) and get an LGTM by someone in [maintainer](MAINTAINERS).
- Create a topic branch from where you want to base your work (usually master).
- Make commits of logical units.
- Make sure your commit messages are in the proper format (see below).
- Push your changes to a topic branch in your fork of the repository.
- Make sure the tests pass, and add any new tests as appropriate.
- Submit a pull request to the original repository.
Thanks for your contributions!
**Note**: When editing documentation you should follow the [CoreOS Docs Style Guide][coreos-docs-style].
## Proposals
For very simple contributions - bug fixes, documentation tweaks, small optimizations, etc. - a proposal is not neccesary. Otherwise, it's necessary to discuss your proposal with other members of the community and get approval from the maintainers.
To create a proposal, copy the markdown from the [proposal template](PROPOSAL_TEMPLATE.md) and put it in the relevant issue. Once you've gotten an LGTM from a [maintainer](MAINTAINERS), you can proceed with putting together a PR.
Don't worry if you're proposal is not accepted right away; you'll probably need to make some changes, depending on the scope of the proposal.
Here's a link which creates a new issue populated with the proposal link:
[Create a Proposal](https://github.com/coreos/dex/issues/new?body=Proposal%0A%3D%3D%3D%0A%0A%28Feel%20free%20to%20change%20headings%20here%2C%20remove%20sections%20that%20are%20not%20relevant%2C%20or%20add%20other%20sections%29%0A%0A%23%23%20Background%0A%0ADescribe%20what%20problem%20is%20being%20solved%20here%2C%20and%20%28briefly%29%20how%20this%20proposal%20solves%20it.%0A%0A%23%23%20Data%20Model%0A%0ADescribe%20the%20logical%20data%20model%20that%20your%20proposal%20adds.%0A%0A%23%23%20Data%20Storage%0A%0ADescribe%20how%20the%20data%20will%20be%20persisted.%0A%0A%23%23%20API%0A%0ADescribe%20the%20methods%20that%20the%20API%20will%20expose.%20If%20there%20are%20any%20breaking%20changes%20be%20sure%20to%20call%20them%20out%20here.%0A%0A%23%23%20UI/UX%0A%0AIs%20there%20a%20front-end%20component%20to%20this%20work%3F%0A%0A%23%23%20Implementation%0A%0AHere%20is%20where%20you%20can%20go%20into%20detail%20about%20implementation%20details%20like%20data%20structures%2C%20algorithms%2C%20etc.%0A%0A%23%23%20Security%0A%0AWhat%20are%20the%20security%20implications%20of%20this%20proposal%3F%20How%20are%20API%20requests%20authenticated%3F%20Who%20can%20make%20API%20calls%3F%0A%0A%23%23%20OIDC/OAUTH2%0A%0ADoes%20this%20feature%20relate%20to%20any%20spec%3F%20%0A%0A%23%23%20Risks/Alternatives%20Considered%0A%0AWhat%20are%20the%20downsides%20to%20this%20implementation%3F%20What%20other%20alternatives%20were%20considered%3F%0A%0A%23%23%20References%0A%0AIf%20there%27s%20any%20references%20or%20prior%20art%2C%20put%20that%20here.)
### Format of the Commit Message
We follow a rough convention for commit messages that is designed to answer two
questions: what changed and why. The subject line should feature the what and
the body of the commit should describe the why.
```
scripts: add the test-cluster command
this uses tmux to setup a test cluster that you can easily kill and
start for debugging.
Fixes #38
```
The format can be described more formally as follows:
```
<subsystem>: <what changed>
<BLANK LINE>
<why this change was made>
<BLANK LINE>
<footer>
```
The first line is the subject and should be no longer than 70 characters, the
second line is always blank, and other lines should be wrapped at 80 characters.
This allows the message to be easier to read on GitHub as well as in various
git tools.
[coreos-docs-style]: https://github.com/coreos/docs/blob/master/STYLE.md

View File

View File

@ -1,72 +1,12 @@
ARG BASE_IMAGE=alpine
FROM quay.io/brianredbeard/corebox
FROM golang:1.18.4-alpine3.15 AS builder
ADD bin/dex-worker /opt/dex/bin/dex-worker
ADD bin/dex-overlord /opt/dex/bin/dex-overlord
ADD bin/dexctl /opt/dex/bin/dexctl
WORKDIR /usr/local/src/dex
ENV DEX_WORKER_HTML_ASSETS /opt/dex/html/
ADD static/html/* $DEX_WORKER_HTML_ASSETS
RUN apk add --no-cache --update alpine-sdk ca-certificates openssl
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT=""
ENV GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT}
ARG GOPROXY
COPY go.mod go.sum ./
COPY api/v2/go.mod api/v2/go.sum ./api/v2/
RUN go mod download
COPY . .
RUN make release-binary
FROM alpine:3.16.2 AS stager
RUN mkdir -p /var/dex
RUN mkdir -p /etc/dex
COPY config.docker.yaml /etc/dex/
FROM alpine:3.16.2 AS gomplate
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
ENV GOMPLATE_VERSION=v3.11.2
RUN wget -O /usr/local/bin/gomplate \
"https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${TARGETOS:-linux}-${TARGETARCH:-amd64}${TARGETVARIANT}" \
&& chmod +x /usr/local/bin/gomplate
# For Dependabot to detect base image versions
FROM alpine:3.16.2 AS alpine
FROM gcr.io/distroless/static:latest AS distroless
FROM $BASE_IMAGE
# Dex connectors, such as GitHub and Google logins require root certificates.
# Proper installations should manage those certificates, but it's a bad user
# experience when this doesn't work out of the box.
#
# See https://go.dev/src/crypto/x509/root_linux.go for Go root CA bundle locations.
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=stager --chown=1001:1001 /var/dex /var/dex
COPY --from=stager --chown=1001:1001 /etc/dex /etc/dex
# Copy module files for CVE scanning / dependency analysis.
COPY --from=builder /usr/local/src/dex/go.mod /usr/local/src/dex/go.sum /usr/local/src/dex/
COPY --from=builder /usr/local/src/dex/api/v2/go.mod /usr/local/src/dex/api/v2/go.sum /usr/local/src/dex/api/v2/
COPY --from=builder /go/bin/dex /usr/local/bin/dex
COPY --from=builder /go/bin/docker-entrypoint /usr/local/bin/docker-entrypoint
COPY --from=builder /usr/local/src/dex/web /srv/dex/web
COPY --from=gomplate /usr/local/bin/gomplate /usr/local/bin/gomplate
USER 1001:1001
ENTRYPOINT ["/usr/local/bin/docker-entrypoint"]
CMD ["dex", "serve", "/etc/dex/config.docker.yaml"]
ENV DEX_WORKER_EMAIL_TEMPLATES /opt/dex/email/
ADD static/email/* $DEX_WORKER_EMAIL_TEMPLATES
ADD static/fixtures/emailer.json $DEX_WORKER_EMAIL_TEMPLATES/emailer.json

74
Documentation/clients.md Normal file
View File

@ -0,0 +1,74 @@
# Clients (\*aka Relying Parties)
## Configuration
Clients can be created in two different ways:
1. Through the [bootstrap API.](https://github.com/coreos/dex/tree/master/schema/adminschema)
1. Through the [Dynamic Registration API.](https://openid.net/specs/openid-connect-registration-1_0.html) That endpoint is hosted at `/registration`
## Dex Features
Dex contains some client features that are not in any OIDC spec, but can be very useful.
## Cross Client Authorization
Inspired by Google's [Cross-Client Identity](https://developers.google.com/identity/protocols/CrossClientAuth), dex also has a way of having one client mint tokens for other ones, called Cross-client authorization.
A client can only mint JWTs for another client if it is a *trusted peer* of that other client. Currently the only way to set trusted peers is the [bootstrap API](https://github.com/coreos/dex/tree/master/schema/adminschema).
To initiate cross-client authentication, add one more scopes of the following form to the initial auth request:
```
audience:server:client_id:$OTHER_CLIENT_ID
```
OTHER\_CLIENT\_ID is the ID of the client for whom you want a token. You can have multiple such scopes in your request, one for each client whom you want the token to be valid for.
After proceeding as normal with the rest of the auth flow, the resulting ID token will have an `aud` field of only the client ID(s) specified by the scope(s). Note that this means this JWT will not have the initiating client's ID in the `aud`; if you want the client's own ID in the `aud`, you must explicitly request it. A client is always implicitly a trusted client of itself.
## Public Clients
There are times when the confidentiality of the client secret cannot be guaranteed; native mobile clients and command-line tools are common examples.
For these cases, *Public Clients* exist, which have certain restrictions:
1. `http://localhost:$PORT` and `urn:ietf:wg:oauth:2.0:oob` are the only valid redirect URIs.
1. A native client cannot obtain *client credentials* from the `/token` endpoint.
These restrictions are aimed at mitigating certain attacks that can arise as the result of having a non-confidenital client secert.
### Creating a public client.
The only way to create a public client is through the [bootstrap API.](https://github.com/coreos/dex/tree/master/schema/adminschema) There are also special requirements for creating a public client:
* A public client must have a client name specified. This is because client name is used in the creation of the client ID for public clients - in confidential clients, the name is dervied from a redirect URI, which public clients do not have.
* Redirect URIs must not be specified; they are implicit.
## Out-Of-Band Auth Flow
For situations in which an app does not have access to a browser, the out-of-band (oob) flow exists. If you specify "urn:ietf:wg:oauth:2.0:oob" as a redirect URI, after authentication, instead of being redirected to the client site, the user is presented with the auth code in a text field, which they must copy and paste ("out of band" as it were) into their app.
\* In OpenID Connect a client is called a "Relying Party", but "client" seems to
be the more common ter, has been around longer and is present in paramter names
like "client_id" so we prefer it over "Relying Party" usually.
## Groups
Connectors that support groups (currently only the LDAP connector) can embed the groups a user belongs to in the ID Token. Using the scope "groups" during the initial redirect with a connector that supports groups will return an JWT with the following field.
```
{
"groups": [
"cn=ipausers,cn=groups,cn=accounts,dc=example,dc=com,
"cn=team-engineering,cn=groups,cn=accounts,dc=example,dc=com"
],
...
}
```
If the client has also requested a refresh token, the groups field is updated during each refresh request.

View File

@ -0,0 +1,321 @@
# Configuring Connectors
Connectors connect dex to authentication providers. dex needs to have at least one connector configured so that users can log in.
## Configuration Format
The dex connector configuration format is a JSON array of objects, each with an ID and type, in addition to whatever other configuration is required, like so:
```
[
{
"id": "local",
"type": "local"
}, {
"id": "Google",
"type": "oidc",
...<<more config>>...
}
]
```
The additional configuration is dependent on the specific type of connector.
### `local` connector
The `local` connector allows email/password based authentication hosted by dex itself. It is special in several ways:
* There can only be one `local` connector in your configuration.
* The id must be `local`
* No other configuration is required
When the `local` connector is present, users can authenticate with the "Log in With Email" button on the authentication screen.
The configuration for the local connector is always the same; it looks like this:
```
{
"id": "local",
"type": "local"
}
```
### `oidc` connector
This connector config lets users authenticate with other OIDC providers. In addition to `id` and `type`, the `oidc` connector takes the following additional fields:
* issuerURL: a `string`. The base URL for the OIDC provider. Should be a URL with an `https` scheme.
* clientID: a `string`. The OIDC client ID.
* clientSecret: a `string`. The OIDC client secret.
* trustedEmailProvider: a `boolean`. If true dex will trust the email address claims from this provider and not require that users verify their emails.
* emailClaim: a `string`. The name of the claim to be treated as an email claim. If empty dex will use a `email` claim.
In order to use the `oidc` connector you must register dex as an OIDC client; this mechanism is different from provider to provider. For Google, follow the instructions at their [developer site](https://developers.google.com/identity/protocols/OpenIDConnect?hl=en). Regardless of your provider, registering your client will also provide you with the client ID and secret.
When registering dex as a client, you need to provide redirect URLs to the provider. dex requires just one:
```
$ISSUER_URL/auth/$CONNECTOR_ID/callback
```
For example runnning a connector with ID `"google"` and an issuer URL of `"https://auth.example.com/foo"` the redirect would be.
```
https://auth.example.com/foo/auth/google/callback
```
Here's what a `oidc` connector looks like configured for authenticating with Google; the clientID and clientSecret shown are not usable. We consider Google a trusted email provider because the email address that is present in claims is for a Google provisioned email account (eg. an `@gmail.com` address)
```
{
"type": "oidc",
"id": "google",
"issuerURL": "https://accounts.google.com",
"clientID": "$DEX_GOOGLE_CLIENT_ID",
"clientSecret": "$DEX_GOOGLE_CLIENT_SECRET",
"trustedEmailProvider": true
}
```
### `github` connector
This connector config lets users authenticate through [GitHub](https://github.com/). In addition to `id` and `type`, the `github` connector takes the following additional fields:
* clientID: a `string`. The GitHub OAuth application client ID.
* clientSecret: a `string`. The GitHub OAuth application client secret.
To begin, register an OAuth application with GitHub through your, or your organization's [account settings](ttps://github.com/settings/applications/new). To register dex as a client of your GitHub application, enter dex's redirect URL under 'Authorization callback URL':
```
$ISSUER_URL/auth/$CONNECTOR_ID/callback
```
For example runnning a connector with ID `"github"` and an issuer URL of `"https://auth.example.com/bar"` the redirect would be.
```
https://auth.example.com/bar/auth/github/callback
```
Here's an example of a `github` connector; the clientID and clientSecret should be replaced by values provided by GitHub.
```
{
"type": "github",
"id": "github",
"clientID": "$DEX_GITHUB_CLIENT_ID",
"clientSecret": "$DEX_GITHUB_CLIENT_SECRET"
}
```
The `github` connector requests read only access to user's email through the [`user:email` scope](https://developer.github.com/v3/oauth/#scopes).
### `bitbucket` connector
This connector config lets users authenticate through [Bitbucket](https://bitbucket.org/). In addition to `id` and `type`, the `bitbucket` connector takes the following additional fields:
* clientID: a `string`. The Bitbucket OAuth consumer client ID.
* clientSecret: a `string`. The Bitbucket OAuth consumer client secret.
To begin, register an OAuth consumer with Bitbucket through your, or your teams's management page. Follow the documentation at their [developer site](https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html).
__NOTE:__ When configuring a consumer through Bitbucket you _must_ configure read email permissions.
To register dex as a client of your Bitbucket consumer, enter dex's redirect URL under 'Callback URL':
```
$ISSUER_URL/auth/$CONNECTOR_ID/callback
```
For example runnning a connector with ID `"bitbucket"` and an issuer URL of `"https://auth.example.com/spaz"` the redirect would be.
```
https://auth.example.com/spaz/auth/bitbucket/callback
```
Here's an example of a `bitbucket` connector; the clientID and clientSecret should be replaced by values provided by Bitbucket.
```
{
"type": "bitbucket",
"id": "bitbucket",
"clientID": "$DEX_BITBUCKET_CLIENT_ID",
"clientSecret": "$DEX_BITBUCKET_CLIENT_SECRET"
}
```
### `ldap` connector
The `ldap` connector allows email/password based authentication hosted by dex, backed by a LDAP directory. The connector can operate in two primary modes:
1. Binding against a specific directory using the end user's credentials.
2. Searching a directory for a entry using a service account then attempting to bind with the user's credentials.
User entries are expected to have an email attribute (configurable through "emailAttribute"), and optionally a display name attribute (configurable through "nameAttribute").
___NOTE:___ Dex currently requires user registration with the dex system, even if that user already has an account with the upstream LDAP system. Installations that use this connector are recommended to provide the "--enable-automatic-registration" flag.
In addition to `id` and `type`, the `ldap` connector takes the following additional fields:
* host: a `string`. The host and port of the LDAP server in form "host:port".
* useTLS: a `boolean`. Whether the LDAP Connector should issue a StartTLS after successfully connecting to the LDAP Server.
* useSSL: a `boolean`. Whether the LDAP Connector should expect the connection to be encrypted, typically used with ldaps port (636/tcp).
* certFile: a `string`. Optional path to x509 client certificate to present to LDAP server.
* keyFile: a `string`. Key associated with x509 client cert specified in `certFile`.
* caFile: a `string`. Filename for PEM-file containing the set of root certificate authorities that the LDAP client use when verifying the server certificates. Default: use the host's root CA set.
* baseDN: a `string`. Base DN from which Bind DN is built and searches are based.
* nameAttribute: a `string`. Entity attribute to map to display name of users. Default: `cn`
* emailAttribute: a `string`. Required. Attribute to map to Email. Default: `mail`
* searchBeforeAuth: a `boolean`. Perform search for entryDN to be used for bind.
* searchFilter: a `string`. Filter to apply to search. Variable substititions: `%u` User supplied username/e-mail address. `%b` BaseDN. Searches that return multiple entries are considered ambiguous and will return an error.
* searchGroupFilter: a `string`. A filter which should return group entry for a given user. The string is formatted the same as `searchFilter`, execpt `%u` is replaced by the fully qualified user entry. Groups are only searched if the client request the "groups" scope.
* searchScope: a `string`. Scope of the search. `base|one|sub`. Default: `one`
* searchBindDN: a `string`. DN to bind as for search operations.
* searchBindPw: a `string`. Password for bind for search operations.
* bindTemplate: a `string`. Template to build bindDN from user supplied credentials. Variable subtitutions: `%u` User supplied username/e-mail address. `%b` BaseDN. Default: `uid=%u,%b`.
### Example: Authenticating against a specific directory
To authenticate against a specific LDAP directory level, use the "bindTemplate" field. This string describes how to map a username to a LDAP entity.
```
{
"type": "ldap",
"id": "ldap",
"host": "127.0.0.1:389",
"baseDN": "cn=users,cn=accounts,dc=auth,dc=example,dc=com",
"bindTemplate": "uid=%u,%d"
}
```
"bindTemplate" is a format string. `%d` is replaced by the value of "baseDN" and `%u` is replaced by the username attempting to login. In this case if a user "janedoe" attempts to authenticate, the bindTemplate will be expanded to:
```
uid=janedoe,cn=users,cn=accounts,dc=auth,dc=example,dc=com
```
The connector then attempts to bind as this entry using the password provided by the end user.
### Example: Searching a FreeIPA server with groups
The following configuration will search a FreeIPA directory using an LDAP filter.
```
{
"type": "ldap",
"id": "ldap",
"host": "127.0.0.1:389",
"baseDN": "cn=accounts,dc=example,dc=com",
"searchBeforeAuth": true,
"searchFilter": "(&(objectClass=person)(uid=%u))",
"searchGroupFilter": "(&(objectClass=ipausergroup)(member=%u))",
"searchScope": "sub",
"searchBindDN": "serviceAccountUser",
"searchBindPw": "serviceAccountPassword"
}
```
"searchFilter" is a format string expanded in a similar manner as "bindTemplate". If the user "janedoe" attempts to authenticate, the connector will run the following query using the service account credentials.
```
(&(objectClass=person)(uid=janedoe))
```
If the search finds an entry, it will attempt to use the provided password to bind as that entry. Searches that return multiple entries are considered ambiguous and will return an error.
"searchGroupFilter" is a format string similar to "searchFilter" except `%u` is replaced by the fully qualified user entry returned by "searchFilter". So if the initial search returns "uid=janedoe,cn=users,cn=accounts,dc=example,dc=com", the connector will use the search query:
```
(&(objectClass=ipausergroup)(member=uid=janedoe,cn=users,cn=accounts,dc=example,dc=com))
```
If the client requests the "groups" scope, the names of all returned entries are added to the ID Token "groups" claim.
## Setting the Configuration
To set a connectors configuration in dex, put it in some temporary file, then use the dexctl command to upload it to dex:
```
dexctl --db-url=$DEX_DB_URL set-connector-configs /tmp/dex_connectors.json
```
### `uaa` connector
This connector config lets users authenticate through the
[CloudFoundry User Account and Authentication (UAA) Server](https://github.com/cloudfoundry/uaa). In addition to `id`
and `type`, the `uaa` connector takes the following additional fields:
* clientID: a `string`. The UAA OAuth application client ID.
* clientSecret: a `string`. The UAA OAuth application client secret.
* serverURL: a `string`. The full URL to the UAA service.
To begin, register an OAuth application with UAA. To register dex as a client of your UAA application, there are two
things your OAuth application needs to have configured properly:
* Make sure dex's redirect URL _(`ISSUER_URL/auth/$CONNECTOR_ID/callback`)_ is in the application's `redirect_uri` list
* Make sure the `openid` scope is in the application's `scope` list
Regarding the `redirect_uri` list, as an example if you were running dex at `https://auth.example.com/bar`, the UAA
OAuth application's `redirect_uri` list would need to contain `https://auth.example.com/bar/auth/uaa/callback`.
Here's an example of a `uaa` connector _(The `clientID` and `clientSecret` should be replaced by values provided to UAA
and the `serverURL` should be the fully-qualified URL to your UAA server)_:
```
{
"type": "uaa",
"id": "example-uaa",
"clientID": "$UAA_OAUTH_APPLICATION_CLIENT_ID",
"clientSecret": "$UAA_OAUTH_APPLICATION_CLIENT_SECRET",
"serverURL": "$UAA_SERVER_URL"
}
```
The `uaa` connector requests only the `openid` scope which allows dex the ability to query the user's identity
information.
### `facebook` connector
This connector config lets users authenticate through [Facebook](https://www.facebook.com/). In addition to `id` and `type`, the `facebook` connector takes the following additional fields:
* clientID: a `string`. The Facebook App ID.
* clientSecret: a `string`. The Facebook App Secret.
To begin, register an App in facebook and configure it according to following steps.
* Go to [https://developers.facebook.com/](https://developers.facebook.com/) and log in using your Facebook credentials.
* If you haven't created developer account follow step 2 in [https://developers.facebook.com/docs/apps/register](https://developers.facebook.com/docs/apps/register).
* Click on `My Apps` and then click `Create a New App`.
* Choose the platform you wish to use. Select `Website` if you are testing dex sample app.
* Enter the name of your new app in the window that appears and click `Create App ID`.
* Enter a `Display Name`, `Contact Email` and select an appropriate `category` from the dropdown. Click `Create App ID`.
* Click on `Dashboard` from the left menu to go to the developer Dashboard. You can find the `App ID` and `App Secret` there. Click Show to view the `App Secret`.
* Click `Settings` on the left menu and navigate to the Basic tab. Add the dex worker domain(if dex is running on localhost, you can add `localhost` as the `App Domain`) and click `Add Platform`.
* Select `Website` as the platform for the application and enter the dex URL (if dex is running on localhost, you can add `http://localhost:5556`). Click `Save Changes`.
* On the left panel, click `Add Product` and click Get Started for a `Facebook Login` product.
* You can configure the Client OAuth Settings on the window that appears. `Client OAuth Login` should be set to `Yes`. `Web OAuth Login` should be set to `Yes`. `Valid OAuth redirect URIs` should be set to in following format.
```
$ISSUER_URL/auth/$CONNECTOR_ID/callback
```
For example runnning a connector with ID `"facebook"` and an issuer URL of `"https://auth.example.com/spaz"` the redirect would be.
```
https://auth.example.com/spaz/auth/facebook/callback
```
* Scroll down and click the Save Changes button to save the change.
Here's an example of a `facebook` connector configuration; the clientID and clientSecret should be replaced by App ID and App Secret values provided by Facebook.
```
{
"type": "facebook",
"id": "facebook",
"clientID": "$DEX_FACEBOOK_CLIENT_ID",
"clientSecret": "$DEX_FACEBOOK_CLIENT_SECRET"
}
```

136
Documentation/dev-guide.md Normal file
View File

@ -0,0 +1,136 @@
# Dev Guide
## No DB mode
When you are working on dex it's convenient to use the `--no-db` flag. This starts up dex in a mode which uses an in-memory datastore for persistence. It also does not rotate keys, so no overlord is required.
In this mode you provide the binary with paths to files for clients, connectors, users, and emailer. There are example files you can use inside of `static/fixtures` named *"clients.json.sample"*, *"connectors.json.sample"*, *"users.json.sample"*, and *"emailer.json.sample"*, respectively.
You can rename these to the equivalent without the *".sample"* suffix since the defaults point to those locations:
```console
cp static/fixtures/clients.json.sample static/fixtures/clients.json
cp static/fixtures/connectors.json.sample static/fixtures/connectors.json
cp static/fixtures/users.json.sample static/fixtures/users.json
cp static/fixtures/emailer.json.sample static/fixtures/emailer.json
```
Starting dex is then as simple as:
```console
bin/dex-worker --no-db
```
***Do not use this flag in production*** - it's not thread safe and data is destroyed when the process dies. In addition, there is no key rotation.
Note: If you want to test out the registration flow, you need to enable that feature by passing `--enable-registration=true` as well.
## Building
To build using the go binary on your host, use the `./build` script.
You can also use a copy of `go` hosted inside a Docker container if you prefix your command with `go-docker`, as in: `./go-docker ./build`
## Docker Build and Push
Once binaries are compiled you can build and push a dex image to quay.io. Before doing this step binaries must be built above using one of the build tools.
```console
./build-docker build
```
If you want to push the build to quay.io, use `./build-docker push`:
```console
export DOCKER_USER=<<your user>>
export DOCKER_PASSWORD=<<your password>>
./build-docker push
```
By default the script pushes to `quay.io/coreos/dex`; if you want to push to a different repository, override the `DOCKER_REGISTRY` and `DOCKER_REPO` environment variables.
## Rebuild API from JSON schema
Go API bindings are generated from a JSON Discovery file.
To regenerate run:
```console
schema/generator
```
For updating generator dependencies see docs in: `schema/generator_import.go`.
## Running Tests
To run all tests (except functional) use the `./test` script;
If you want to test a single package only, use `PKG=<pkgname> ./test`
The functional tests require a database; create a database (eg. `createdb dex_func_test`) and then pass it as an environment variable to the functional test script, eg. `DEX_TEST_DSN=postgres://localhost/dex_func_test?sslmode=disable ./test-functional`
To run these tests with Docker is a little trickier; you need to have a container running Postgres, and then you need to link that container to the container running your tests:
```console
# Run the Postgres docker container, which creates a db called "postgres"
docker run --name dex_postgres -d postgres
# The host name in the DSN is "postgres"; that works because that is what we
# will alias the link as, which causes Docker to modify /etc/hosts with a "postgres"
# entry.
export DEX_TEST_DSN=postgres://postgres@postgres/postgres?sslmode=disable
# Run the test container, linking it to the Postgres container.
DOCKER_LINKS=dex_postgres:postgres DOCKER_ENV=DEX_TEST_DSN ./go-docker ./test-functional
# Remove the container after the tests are run.
docker rm -f dex_postgres
```
## Vendoring dependencies
dex uses [glide](https://github.com/Masterminds/glide) for vendoring external dependencies. This section details how to add and update those dependencies.
Before continuing, please ensure you have the **latest version** of glide available in your PATH.
```
go get -u github.com/Masterminds/glide
```
### Adding a new package
After adding a new `import` to dex source, use `glide get` to add the dependency to the `glide.yaml` and `glide.lock` files.
```
glide get -u -v -s github.com/godbus/dbus
```
Note that __all of these flags are manditory__. This should add an entry to the glide files, add the package to the `vendor` directory, and remove nested `vendor` directories and version control information.
## Updating an existing package
To update an existing package, edit the `glide.yaml` file to the desired verison (most likely a git hash), and run `glide update`.
```
{{ edit the entry in glide.yaml }}
glide update -u -v -s github.com/lib/pq
```
Like `glide get` all flags are manditory. If the update was successful, `glide.lock` will have been updated to reflect the changes to `glide.yaml` and the package will have been updated in `vendor`.
## Finalizing your change
Use git to ensure the `vendor` directory has updated only your target packages, and that no other entries in `glide.yaml` and `glide.lock` have changed.
Changes to the Godeps directory should be added as a separate commit from other changes for readability:
```
git status # make sure things look reasonable
git add vendor
git commit -m "vendor: updated postgres driver"
# continue working
git add .
git commit -m "dirname: this is my actual change"
```

View File

@ -0,0 +1,52 @@
# Configuring Sending Emails
Dex sends emails to a during the registration process to verify an email
address belongs to the person signing up. Currently Dex supports two ways of
sending emails, and has a third option for use during development.
Configuration of the email provider in Dex is provided through a JSON file. All
email providers have a `type` and `id` field as well as some additional provider
specific fields.
## SMTP
If using SMTP the `type` field **must** be set to `smtp`. Additionally both
`host` and `port` are required. If you wish to use SMTP plain auth, then
specify your username and password.
```
{
"type": "smtp",
"host": "smtp.example.org:587",
"from": "postmaster@example.com",
"username": "postmaster@example.org",
"password": "foo"
}
```
## Mailgun
If using Mailgun the `type` field **must** be set to `mailgun`. Additionally
`privateAPIKey`, `publicAPIKey`, and `domain` are required.
```
{
"type": "mailgun",
"from": "noreply@example.com",
"privateAPIKey": "key-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"publicAPIKey": "YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY",
"domain": "sandboxZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ.mailgun.org"
}
```
## Dev
The fake emailer should only be used in development. The fake emailer
prints emails to `stdout` rather than sending any email. If using the fake
emailer the `type` field **must** be set to `fake`.
```
{
"type": "fake"
}
```

View File

@ -0,0 +1,164 @@
# Getting Started
# Introduction
In this document we'll stand up the full dex stack on a single machine. This should demonstrate all the moving parts involved in a dex installation, but is not appropriate for production deployment. Please see the [deployment guide][deployment-guide] for information on production dex setups.
[deployment-guide]: https://github.com/coreos/dex/blob/master/Documentation/deploy-guide.md
We'll also start the example web app, so we can try registering and logging in.
# Pre-requisites
Before continuing, you must have the following installed on your system:
* Go 1.4 or greater
* Postgres 9.4 or greater (this guide also assumes that Postgres is up and running)
In addition, if you wish to try out authenticating against Google's OIDC backend, you must have a new client registered with Google:
* Go to https://console.developers.google.com/project and select an existing project or create a new project.
* Click on APIs and auth > Credentials, and select an OAuth 2 client ID from the Add credentials dropdown.
* On the "Create Client ID" screen, choose "Web Application", provide a Name and enter `http://127.0.0.1:5556/dex/auth/google/callback` for your Authorised redirect URI.
* The generated client ID and client secret will be needed later.
# Create Database
On the PostgreSQL server, login as a user with appropriate permissions and create a database and user for dex to use. These can be named arbitrarily, but are called `dex_db` and `dex`, respectively, in this example.
```sql
CREATE DATABASE dex_db;
CREATE USER dex WITH PASSWORD 'dex_pass';
GRANT ALL PRIVILEGES ON DATABASE dex_db TO dex;
```
Store the [connection string](http://www.postgresql.org/docs/9.4/static/libpq-connect.html#LIBPQ-CONNSTRING) for the dex database in an environment variable:
```
DEX_DB_URL=postgres://dex:dex_pass@localhost/dex_db?sslmode=disable
```
# Building
The build script will build all dex components.
`./build`
# Generate a Secret Symmetric Key
dex needs a 32 byte base64-encoded key which will be used to encrypt the private keys in the database. A good way to generate the key is to read from /dev/random:
`DEX_KEY_SECRET=$(dd if=/dev/random bs=1 count=32 2>/dev/null | base64 | tr -d '\n')`
The dex overlord and workers allow multiple key secrets (separated by commas) to be passed but only the first will be used to encrypt data; the rest are there for decryption only; this scheme allows for the rotation of keys without downtime (assuming a rolling restart of workers).
# Generate an Admin API Secret
The dex overlord has an API which is very powerful - you can create Admin users with it, so it needs to be protected somehow. This is accomplished by requiring that a secret is passed via the Authorization header of each request. This secret is 128 bytes base64 encoded, and should be sufficiently random so as to make guessing impractical:
`DEX_OVERLORD_ADMIN_API_SECRET=$(dd if=/dev/random bs=1 count=128 2>/dev/null | base64 | tr -d '\n')`
# Start the overlord
The overlord is responsible for creating and rotating keys and some other administrative tasks. In addition, the overlord is responsible for creating the necessary database tables (and when you update, performing schema migrations), so it must be started before we do anything else. Debug logging is turned on so we can see more of what's going on. Start it up.
`./bin/dex-overlord --admin-api-secret=$DEX_OVERLORD_ADMIN_API_SECRET --db-url=$DEX_DB_URL --key-secrets=$DEX_KEY_SECRET --log-debug=true &`
## Environment Variables.
Note that parameters can be passed as flags or environment variables to dex components; an equivalent start with environment variables would be:
```
export DEX_OVERLORD_ADMIN_API_SECRET=$DEX_OVERLORD_ADMIN_API_SECRET
export DEX_OVERLORD_DB_URL=$DEX_DB_URL
export DEX_OVERLORD_KEY_SECRETS=$DEX_KEY_SECRET
export DEX_OVERLORD_LOG_DEBUG=true
./bin/dex-overlord &
```
# Start the dex-worker
Before starting `dex-worker` you should determine how you want verification emails to be delivered to the user.
If you just want to test dex out, you can just use the provided sample config in `static/fixtures/emailer.json.sample`.
Please review [email-configuration](https://github.com/coreos/dex/blob/master/Documentation/email-configuration.md) for details
(make sure you point `--email-cfg` to your newly configured file).
Once you have setup your email config run `dex-worker`:
`./bin/dex-worker --db-url=$DEX_DB_URL --key-secrets=$DEX_KEY_SECRET --email-cfg=static/fixtures/emailer.json.sample --enable-registration=true --log-debug=true &`
Now you have a worker which you can authenticate against, listening on `http://0.0.0.0:5556`, which is the default. Note that the default issuer URL (which can be changed on --issuer) is `http://127.0.0.1:5556`. The issuer URL is the base URL (i.e. no query or fragments) uniquely identifying your dex installation.
Note: the issuer URL MUST have an `https` scheme in production to meet spec compliance and to be considered reasonably secure.
# Set up Connectors
The worker and overlord are up and running, but we need to tell dex what connectors we want to use to authenticate. For this case we'll set up a local connector, where dex manages credentials and provides a UI for authentication, and a Google OIDC connector.
If you prefer to use the Google OIDC Identity Provider (IdP), just omit the second entry in the JSON connector list. Note that you must replace `DEX_GOOGLE_CLIENT_SECRET` and `DEX_GOOGLE_CLIENT_ID` with the client secret and client ID you got when you registered your project with the Google developer console.
```
cat << EOF > /tmp/dex_connectors.json
[
{
"type": "local",
"id": "local"
},
{
"type": "oidc",
"id": "google",
"issuerURL": "https://accounts.google.com",
"clientID": "$DEX_GOOGLE_CLIENT_ID",
"clientSecret": "$DEX_GOOGLE_CLIENT_SECRET",
"trustedEmailProvider": true
}
]
EOF
./bin/dexctl --db-url=$DEX_DB_URL set-connector-configs /tmp/dex_connectors.json
```
One thing to note here that's a bit confusing here is that in the case of the Google OIDC connector, dex is the client and Google is the IdP, but when you're dealing with your own apps that want to authenticate against dex, your app is the client and dex is the IdP.
# Register a Client
Like all OAuth2/OIDC IdPs, clients must be registered with the IdP (dex), along with their valid redirect URLS.
New clients can be registered with the dexctl CLI tool:
```
eval "$(./bin/dexctl --db-url=$DEX_DB_URL new-client http://127.0.0.1:5555/callback)"
```
The output of this command is eval'able if you are in bash, and sets the following shell variables:
```
DEX_APP_CLIENT_ID
DEX_APP_CLIENT_SECRET
```
# Start the Example Web App
The included example app demonstrates registering and authenticating with dex. Start it up:
```
./bin/example-app --client-id=$DEX_APP_CLIENT_ID --client-secret=$DEX_APP_CLIENT_SECRET --discovery=http://127.0.0.1:5556/dex &
```
# Authenticate with dex!
Go to `127.0.0.1:5555`, and click "register"; choose either "Google", if you have a Google Account and would like to use that to authenticate. Otherwise, choose "local".
If you chose Google, enter your credentials (if you are not logged into Google) and click through the authorization screen. If you chose "local", enter a name and password and submit.
After registering you should end up back at the example app, where it will display the claims returned by dex.
# Verify Your Email
If you registered with Google, your email address is already verified, and this should be reflected by the presence of an `email_verified` claim. Otherwise, you need to verify your email address.
In a fully configured production environment an email provider will be set up so that dex can email users email verification links (amongst other things); in this setup, we are using the `FakeEmailer` email provider which simply outputs to stdout. Look for the "Welcome to Dex!" message in your console and copy the link that follows it, and then paste it in your browser; you should end up back at the example app page that displays claims, but this time you'll see a tru `email_verified` claim.
# Standup Dev Script
A script which does almost everything in this guide exists at `contrib/standup-db.sh`. Read the comments inside before attempting to run it - it requires a little setup beforehand.

42
Documentation/oauth2.md Normal file
View File

@ -0,0 +1,42 @@
# dex OAuth 2.0 Implementation
OAuth 2.0 is defined in [RFC 6749][rfc6749]. The RFC defines the bare minimum necessary to implement OAuth 2.0, while it also describes a set of optional behaviors. This document aims to describe what decisions have been made in dex with respect to those optional behaviors.
[rfc6749]: https://tools.ietf.org/html/rfc6749
While the goal of dex is to accurately implement the required aspects of the OAuth 2.0 specification, dex is still under active development and certain disrepancies exist. Any such discrepancies are documented below.
## TLS
It is a requirement of OAuth 2.0 (RFC 6749 Section 2.3.1) that any authorization server utilize TLS to protect sensitive information (e.g. client passwords) transmitted between remote parties during the OAuth workflow.
From a practical standpoint, TLS can be a tedious requirement for development environments.
It also may be desirable to deploy dex behind an SSL-terminating load balancer.
dex does not require the use of TLS, but it should be considered necessary when deploying dex on any public networks.
## Client Authentication
Unregistered clients are not supported (RFC 6749 Section 2.4).
## Authorization Endpoint
User-agent MUST make a valid authorization request to the authorization endpoint (RFC 6749 Section 3.1) using the "authorization code" grant type; no other grant types are supported.
Additionally, GET w/ query parameters must be used - POST w/ a form in the request body body is not supported.
The OAuth 2.0 spec leaves the implementation details of the authorization step up to the implementer as long as the request and response formats are respected.
dex relies on remote identity providers to fulfill the actual authorization of user-agents.
This federated approach typically requires one or more additional HTTP redirects and a manual login step past what is encountered with a typical OAuth 2.0 server.
Given this detail, it is necessary that user-agents follow all HTTP redirects during an authorization attempt.
Additionally, the HTTP response from the initial authorization request will likely not redirect the user-agent to the redirection endpoint provided in that initial request.
User-agent MUST not reject redirections to unrecognized endpoints.
## Token endpoint
Clients MUST identify themselves using the Basic HTTP authentication scheme (RFC 6749 Section 2.3.1).
Given this requirement, the client_id and client_secret fields of the request are ignored.
Refresh tokens are never generated and returned.
Given that the authorization endpoint only supports authorization codes and refresh tokens are never generated, the only supported values of grant_type are "authorization_code" and "client_credentials".

View File

@ -0,0 +1,66 @@
# OIDC Connect Core Notes
dex aims to be a full featured OIDC Provider, but it still has a little ways to go. Most of the places where dex and OIDC diverge are minor, but we do want to fix them. Here's a list of these places; there may be other discrepancies as well if you find any please file an issue or even better, a pull request.
To be clear: the places were we are not in compliance with mandatory features, we will fix. As for things marked as OPTIONAL in the spec, whether and when those are supported by dex will be driven by the needs of the community.
# Notes on [OpenID Connect Core](http://openid.net/specs/openid-connect-core-1_0.html)
Sec. 2. [ID Token](http://openid.net/specs/openid-connect-core-1_0.html#IDToken)
- None of the OPTIONAL claims (`acr`, `amr`, `azp`, `auth_time`) are supported
- dex signs using JWS but does not do the OPTIONAL encryption.
Sec. 3. [Authentication](http://openid.net/specs/openid-connect-core-1_0.html#Authentication)
- Only the authorization code flow (where `response_type` is `code`) is supported.
Sec. 3.1.2.1. [Authentication Request](http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest)
- max_age not implemented; it's OPTIONAL in the spec, but if it's present servers MUST include auth_time, which dex does not.
- None of the other OPTIONAL parameters are implemented with the exception of:
- state
- nonce
- dex also defines a non-standard `register` parameter; when this parameter is `1`, end-users are taken through a registration flow, which after completing successfully, lands them at the specified `redirect_uri`
Sec. 3.2.2.3. [Authorization Server Authenticates End-User](http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthenticates)
- The spec states that the authentication server "MUST NOT interact with the End-User" when `prompt` is `none` We don't check the prompt parameter at all; similarly, dex MUST re-prompt when `prompt` is `login` - dex does not do this either.
Sec. 3.1.3.2. [Token Request Validation](http://openid.net/specs/openid-connect-core-1_0.html#TokenRequestValidation)
- In Token requests, dex chooses to proceed without error when `redirect_uri` is not present and there's only one registered valid URI (which is valid behavior)
Sec. 4. [Initiating Login from a Third Party](http://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin)
- dex does not support this at this time
Sec. 5.1.2. [AdditionalClaims](http://openid.net/specs/openid-connect-core-1_0.html#AdditionalClaims)
- dex defines uses the following additional claims:
- `http://coreos.com/password/old-hash`
- `http://coreos.com/password/reset-callback`
- `http://coreos.com/email/verification-callback`
- `http://coreos.com/email/verificationEmail`
Sec. 5.3. [UserInfo Endpoint](http://openid.net/specs/openid-connect-core-1_0.html#UserInfo)
- dex does not implement this endpoint.
Sec. 6.1 [Passing a Request Object by Value](http://openid.net/specs/openid-connect-core-1_0.html#JWTRequests)
- dex does not implement this feature.
Sec. 7. [Self-Issued OpenID Provider](http://openid.net/specs/openid-connect-core-1_0.html#SelfIssued)
- dex does not implement this feature.
Sec. 8. [Subject Identifier Types](http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes)
- dex only supports the `public` subject identifier type.
Sec. 9. [Client Authentication](http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication)
- dex only supports the `client_secret_basic` client authentication type.
Sec. 11. [Offline Access](http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess)
- offline_access in 'scope' is supported, but as we haven't implemented 'prompt' yet, the
spec's requirement is not fully met yet.
Sec. 15.1. [Mandatory to Implement Features for All OpenID Providers](http://openid.net/specs/openid-connect-core-1_0.html#ImplementationConsiderations)
- dex is missing the follow mandatory features (some are already noted elsewhere in this document):
- Support for `prompt` parameter
- Support for the `auth_time` parameter
- Support for enforcing `max_age` parameter
Sec. 15.3. [Discovery and Registration](http://openid.net/specs/openid-connect-core-1_0.html#DiscoReg)
- dex supports OIDC Discovery at the standard `/.well-known/openid-configuration` endpoint.

View File

@ -0,0 +1,5 @@
# Proposals
This directory holds design documents for proposed changes to dex. It is meant for early feedback on features and _does not reflect active development_.
For a list of more immediate development goals, see dex's [roadmap](../roadmap.md).

View File

@ -0,0 +1,198 @@
Authorization (Auth-Z) Proposal
===============================
## Components
Core-Auth consisists of various components:
1. A web app for users to authenticate with.
2. A web app for users to manage auth-z policies.
3. An API to serve auth-z queries.
4. A common golang library to: validate auth-n tokens, assert identities from auth-n tokens, and fetch auth-z policies for users.
## Design Strategy
- Users authenticate and are provided a JWT
- API keys are the same format JWT
- Apps use the common library to access core-auth API to fetch auth-z policies etc
- Auth-z policy requests supply an etag, policies are cached with a ttl
## Basic Flow
1. users log in via OAuth and are redirected to app with token (alternatively entities can use pregenerated api tokens)
1. app uses common lib to assert identity from token
1. app uses common lib to fetch auth-z policies (if not cached, or ttl expired) from configured auth-z server
1. app uses common lib to assert permissions to requested resource(s)
input: policies, resource CRN(s)
output: yes/no, or filtered list of CRNs
1. app responds with: denial, full-results, or filter-results
## Permissions Specification
### Core Resource Namespaces (CRN)
Format: `crn:provider:product:instance:resource-type:resource`
- `provider`: a unique FQDN of the organization that created/maintains the application
- `product`: a product id/name unique to the provider
- `instance`: an individual deployed instance of the product (FQDN, or UUID?)
- `resource-type`: app specific resource type unqiue to the product
- `resource`: the uniqe id of the resource in question (can use `/` for nested resources)
### Resource Namespace Examples
CoreUpdate Examples:
```
// CoreOS App
crn:coreos.com:coreupdate:public.update.core-os.net:app:e96281a6-d1af-4bde-9a0a-97b76e56dc57
// CoreOS App's "stable" Group
crn:coreos.com:coreupdate:public.update.core-os.net:group:e96281a6-d1af-4bde-9a0a-97b76e56dc57/stable
```
Quay Example:
```
crn:quay.io:enterprise-registry:my-registry.my-company.com:repo:hello-world
```
### Actions
Similar to CRN but defined and registered by individual apps.
Action describes the type of access that should be allowed or denied (for example, read, write, list, delete, startService, and so on)
```
provider:product:name
```
- `provider`: a unique FQDN of the organization that created/maintains the application (same as CRN)
- `product`: a product id/name unique to the provider (same as CRN)
- `name`: the acutal name of the aciton in the product
#### Action Examples
CoreUpdate (general):
```
coreos.com:coreupdate:read
coreos.com:coreupdate:write
coreos.com:coreupdate:delete
```
CoreUpdate (finer grained control):
```
coreos.com:coreupdate:publish
coreos.com:coreupdate:pause
coreos.com:coreupdate:modifyBehavior
coreos.com:coreupdate:modifyChannel
coreos.com:coreupdate:modifyVersion
```
### Policies
```json
{
"apiVersion": "v1",
"id": "7CFCE45E-610A-407C-84DB-86A24658B217",
"label": "my policy",
"statements": [
{
"effect": "allow",
"resource": ["crn:..."],
"action": ["crn:..."],
},
]
}
```
Policy:
- `apiVersion`: the policy API version
- `id`: unique id of the policy
- `label`: human readable label for the policy
- `statements`: the main element for a policy. it is a list of multiple statements defining access.
Statement:
- `effect`: "allow" or "deny"
- `resource`: the CRN of the resource
- `action`: described in action section
#### Policiy Examples
CoreUpdate Example:
```json
{
"apiVersion": "v1",
"id": "rando-uuid",
"label": "admin",
"description": "allows full admin access to everything",
"statements": [
{
"effect": "allow",
"resource": ["crn:coreos.com:coreupdate:public.update.core-os.net:*:*"],
"action": ["coreos.com:coreupdate:*"],
},
]
}
{
"apiVersion": "v1",
"id": "rando-uuid",
"label": "full-read-only",
"description": "allows read only access to everything",
"statements": [
{
"effect": "allow",
"resource": ["crn:coreos.com:coreupdate:public.update.core-os.net:*:*"],
"action": ["coreos.com:coreupdate:read"],
},
]
}
{
"apiVersion": "v1",
"id": "rando-uuid",
"label": "full-internal-only",
"description": "allows read access to everything, denys write access to the main CoreOS app",
"statements": [
{
"effect": "allow",
"resource": ["crn:coreos.com:coreupdate:public.update.core-os.net:*:*"],
"action": ["coreos.com:coreupdate:read"],
},
{
"effect": "deny",
"resource": ["crn:coreos.com:coreupdate:public.update.core-os.net:app:e96281a6-d1af-4bde-9a0a-97b76e56dc57"],
"action": ["coreos.com:coreupdate:write"],
},
]
}
```
### Policy Associations
Policies can be attached to Organizations, Groups, or Users.
### Core Auth API
Should support the following operations:
- create/delete actions
- create/delete resources
- get policies for entity
- TBD
- TBD
# Open Questions
- Do we support resource-based permissions?
- Do we need to include the org in the CRNs?
- Do we version the policies (different than apiVersion) for easy undo/history?
- Should we have an `enabled` flag on policies?
- How to handle public anonymous access?
- Include the AWS equivalent of `NotResource` and `NotAction`?
- Do we need to support conditions in policies? Perhaps we don't need these for v1.

37
Documentation/releases.md Normal file
View File

@ -0,0 +1,37 @@
# Making a dex Release
Make sure you've [uploaded your GPG key](https://github.com/settings/keys) and
configured git to [use that signing key](
https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work) either globally or
for the Dex repo. Note that the email the key is issued for must be the email
you use for git.
```
git config [--global] user.signingkey "{{ GPG key ID }}"
git config [--global] user.email "{{ Email associated with key }}"
```
Create a signed tag at the commit you wish to release. This action will prompt
you to enter a tag message, which can just be the release version.
```
git tag -s v0.4.0 ea4c04fde83bd6c48f4d43862c406deb4ea9dba2
```
Push that tag to the CoreOS repo.
```
git push git@github.com:coreos/dex.git v0.4.0
```
Draft releases on GitHub and summarize the changes since the last release. See
previous releases for the expected format.
https://github.com/coreos/dex/releases
Finally create an image tag on Quay corresponding to the release. Log into
Quay, navigate to the `quay.io/coreos/dex` repo, find the correct commit, and
add an additional tag to that image for the release (click the gear on the
image tag's row and then "Add New Tag").
https://quay.io/repository/coreos/dex?tag=latest&tab=tags

31
Documentation/roadmap.md Normal file
View File

@ -0,0 +1,31 @@
# dex 0.4 Roadmap
These are the roadmap items for the dex team over the 0.4 release cycle (in no particular order).
## Groups
Start work on groups.
* Add groups ([#175](https://github.com/coreos/dex/issues/175))
## Refresh tokens
Finish work on refresh token revocation.
* API endpoints for revoking refresh tokens ([#261](https://github.com/coreos/dex/issues/261))
## dexctl rework
Deprecating dexctls --db-url flag. Achieve feature parity between existing commands and the bootstrapping API, then have all dexctl actions go through that.
* Overarching issue of deprecating --db-url flag ([#298](https://github.com/coreos/dex/issues/298))
* Add client registration to bootstrapping API ([#326](https://github.com/coreos/dex/issues/326))
* Set connector configs through bootstrapping API ([#360](https://github.com/coreos/dex/issues/360))
## Further server side cleanups
Establish idioms for handling HTTP requests, create a storage interface for backends, and continue to improve --no-db mode.
* Improve server code and storage interfaces ([#278](https://github.com/coreos/dex/issues/278))
* Fix client secrets encoding in --no-db mode ([#337](https://github.com/coreos/dex/issues/337))
* Easier specification of passwords in --no-db mode ([#340](https://github.com/coreos/dex/issues/340))

View File

@ -0,0 +1,59 @@
# TLS Setup
According to the [OpenID Connect Spec](http://openid.net/specs/openid-connect-core-1_0.html),
TLS [is required](http://openid.net/specs/openid-connect-core-1_0.html#TLSRequirements) for connection between the client and the dex server.
This guide explains how to set up a TLS connection for dex.
# Create certificates, key files.
To get up and running you will need:
- Certificate Authority file (CA cert).
- Certificate file for the server signed by the CA above.
- Private Key file of the server, used by the server to exchange keys during TLS handshake.
There are a lot of tools and guides available to help you generate these files:
- Use 'openssl' command line. The guide can be found [here](http://www.g-loaded.eu/2005/11/10/be-your-own-ca/)
- Use [etcd-ca](https://github.com/coreos/etcd-ca), which is a simple certificate manager written in Go. Despite the its name, it can be used in other cases than etcd as well.
- Use Cloudflare's [cfssl](https://github.com/cloudflare/cfssl), we also provide example configs [here](../examples/tls-setup), which is as simple as run `make`.
# Start the server using TLS
Assume we already generated a CA file, a server certificate and a key file, then in order to make the dex server accept TLS connections, we will run the `dex-worker` with `--cert-file=$PATH_TO_SERVER_CERTIFICATE` and `--key-file=$PATH_TO_SERVER_KEY_FILE`, For example:
```shell
./build
./bin/dex-worker \
--cert-file=examples/tls-setup/certs/dex.pem \
--key-file=examples/tls-setup/certs/dex-key.pem \
--listen="https://127.0.0.1:5556" \
--issuer="https://127.0.0.1:5556" \
--clients=./static/fixtures/clients.json \
--connectors=./static/fixtures/connectors.json.sample \
--email-cfg=./static/fixtures/emailer.json.sample \
--users=./static/fixtures/users.json.sample \
--no-db
```
Where: <br/>
`--cert-file` and `--key-file` tells the dex-worker which certificate and key file to use. <br/>
`--listen` tells dex-worker where to receive requests. <br/>
`--clients`, `--connectors`, `--email-cfg` and `--users` tells dex-worker where to find user/client data when database access is not enabled (`--no-db`). <br/>
When establishing connection to the server, we will need to provide the CA file to client unless the server's ceritificate is signed by CAs that already trusted by client's machine. For example:
```shell
./build
./bin/example-app \
--trusted-ca-file=examples/tls-setup/certs/ca.pem \
--client-id="XXX" \
--client-secret="c2VjcmV0ZQ==" \
--redirect-url="http://127.0.0.1:5555/callback" \
--discovery="https://127.0.0.1:5556" \
--listen="http://127.0.0.1:5555"
```
Where: <br/>
`--trusted-ca-file` tells the app where to find the CA file that we will trust. Note that if the server's certificate is self-signed, then the server's certificate is also the CA file here. <br/>
`--client-id`, `--client-secret` and `--redirect-url` are the registered client's identity information. <br/>
`--discovery` and `--listen` tell the app where to connect to the server and where to handle requests. <br/>
Next, you can go to http://127.0.0.1:5555 to register, login and enjoy your OIDC tokens generated by dex server.

View File

@ -1,5 +1,4 @@
Apache License
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@ -179,7 +178,7 @@
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
@ -187,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -200,3 +199,4 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,6 +1,3 @@
Joel Speed <Joel.speed@hotmail.co.uk> (@JoelSpeed)
Maksim Nabokikh <max.nabokih@gmail.com> (@nabokihms)
Mark Sagi-Kazar <mark.sagikazar@gmail.com> (@sagikazarmark)
Nandor Kracser <bonifaido@gmail.com> (@bonifaido)
Rithu John <rithujohn191@gmail.com> (@rithujohn191)
Stephen Augustus <foo@auggie.dev> (@justaugustus)
Bobby Rullo <bobby.rullo@coreos.com> (@bobbyrullo)
Ed Rooth <ed.rooth@coreos.com> (@sym3tri)
Eric Chiang <eric.chiang@coreos.com> (@ericchiang)

162
Makefile
View File

@ -1,162 +0,0 @@
OS = $(shell uname | tr A-Z a-z)
export PATH := $(abspath bin/protoc/bin/):$(abspath bin/):${PATH}
PROJ=dex
ORG_PATH=github.com/dexidp
REPO_PATH=$(ORG_PATH)/$(PROJ)
VERSION ?= $(shell ./scripts/git-version)
DOCKER_REPO=quay.io/dexidp/dex
DOCKER_IMAGE=$(DOCKER_REPO):$(VERSION)
$( shell mkdir -p bin )
user=$(shell id -u -n)
group=$(shell id -g -n)
export GOBIN=$(PWD)/bin
LD_FLAGS="-w -X main.version=$(VERSION)"
# Dependency versions
KIND_NODE_IMAGE = "kindest/node:v1.19.11@sha256:07db187ae84b4b7de440a73886f008cf903fcf5764ba8106a9fd5243d6f32729"
KIND_TMP_DIR = "$(PWD)/bin/test/dex-kind-kubeconfig"
.PHONY: generate
generate:
@go generate $(REPO_PATH)/storage/ent/
build: generate bin/dex
bin/dex:
@mkdir -p bin/
@go install -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex
examples: bin/grpc-client bin/example-app
bin/grpc-client:
@mkdir -p bin/
@cd examples/ && go install -v -ldflags $(LD_FLAGS) $(REPO_PATH)/examples/grpc-client
bin/example-app:
@mkdir -p bin/
@cd examples/ && go install -v -ldflags $(LD_FLAGS) $(REPO_PATH)/examples/example-app
.PHONY: release-binary
release-binary: LD_FLAGS = "-w -X main.version=$(VERSION) -extldflags \"-static\""
release-binary: generate
@go build -o /go/bin/dex -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex
@go build -o /go/bin/docker-entrypoint -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/docker-entrypoint
docker-compose.override.yaml:
cp docker-compose.override.yaml.dist docker-compose.override.yaml
.PHONY: up
up: docker-compose.override.yaml ## Launch the development environment
@ if [ docker-compose.override.yaml -ot docker-compose.override.yaml.dist ]; then diff -u docker-compose.override.yaml docker-compose.override.yaml.dist || (echo "!!! The distributed docker-compose.override.yaml example changed. Please update your file accordingly (or at least touch it). !!!" && false); fi
docker-compose up -d
.PHONY: down
down: clear ## Destroy the development environment
docker-compose down --volumes --remove-orphans --rmi local
test:
@go test -v ./...
testrace:
@go test -v --race ./...
.PHONY: kind-up kind-down kind-tests
kind-up:
@mkdir -p bin/test
@kind create cluster --image ${KIND_NODE_IMAGE} --kubeconfig ${KIND_TMP_DIR}
kind-down:
@kind delete cluster
rm ${KIND_TMP_DIR}
kind-tests: export DEX_KUBERNETES_CONFIG_PATH=${KIND_TMP_DIR}
kind-tests: testall
.PHONY: lint lint-fix
lint: ## Run linter
golangci-lint run
.PHONY: fix
fix: ## Fix lint violations
golangci-lint run --fix
.PHONY: docker-image
docker-image:
@sudo docker build -t $(DOCKER_IMAGE) .
.PHONY: verify-proto
verify-proto: proto
@./scripts/git-diff
clean:
@rm -rf bin/
testall: testrace
FORCE:
.PHONY: test testrace testall
.PHONY: proto
proto:
@protoc --go_out=paths=source_relative:. --go-grpc_out=paths=source_relative:. api/v2/*.proto
@protoc --go_out=paths=source_relative:. --go-grpc_out=paths=source_relative:. api/*.proto
#@cp api/v2/*.proto api/
.PHONY: proto-internal
proto-internal:
@protoc --go_out=paths=source_relative:. server/internal/*.proto
# Dependency versions
GOLANGCI_VERSION = 1.46.0
GOTESTSUM_VERSION ?= 1.7.0
PROTOC_VERSION = 3.15.6
PROTOC_GEN_GO_VERSION = 1.26.0
PROTOC_GEN_GO_GRPC_VERSION = 1.1.0
KIND_VERSION = 0.11.1
deps: bin/gotestsum bin/golangci-lint bin/protoc bin/protoc-gen-go bin/protoc-gen-go-grpc bin/kind
bin/gotestsum:
@mkdir -p bin
curl -L https://github.com/gotestyourself/gotestsum/releases/download/v${GOTESTSUM_VERSION}/gotestsum_${GOTESTSUM_VERSION}_$(shell uname | tr A-Z a-z)_amd64.tar.gz | tar -zOxf - gotestsum > ./bin/gotestsum
@chmod +x ./bin/gotestsum
bin/golangci-lint:
@mkdir -p bin
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | BINARY=golangci-lint bash -s -- v${GOLANGCI_VERSION}
bin/protoc:
@mkdir -p bin/protoc
ifeq ($(shell uname | tr A-Z a-z), darwin)
curl -L https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-osx-x86_64.zip > bin/protoc.zip
endif
ifeq ($(shell uname | tr A-Z a-z), linux)
curl -L https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-x86_64.zip > bin/protoc.zip
endif
unzip bin/protoc.zip -d bin/protoc
rm bin/protoc.zip
bin/protoc-gen-go:
@mkdir -p bin
curl -L https://github.com/protocolbuffers/protobuf-go/releases/download/v${PROTOC_GEN_GO_VERSION}/protoc-gen-go.v${PROTOC_GEN_GO_VERSION}.$(shell uname | tr A-Z a-z).amd64.tar.gz | tar -zOxf - protoc-gen-go > ./bin/protoc-gen-go
@chmod +x ./bin/protoc-gen-go
bin/protoc-gen-go-grpc:
@mkdir -p bin
curl -L https://github.com/grpc/grpc-go/releases/download/cmd/protoc-gen-go-grpc/v${PROTOC_GEN_GO_GRPC_VERSION}/protoc-gen-go-grpc.v${PROTOC_GEN_GO_GRPC_VERSION}.$(shell uname | tr A-Z a-z).amd64.tar.gz | tar -zOxf - ./protoc-gen-go-grpc > ./bin/protoc-gen-go-grpc
@chmod +x ./bin/protoc-gen-go-grpc
bin/kind:
@mkdir -p bin
curl -L https://github.com/kubernetes-sigs/kind/releases/download/v${KIND_VERSION}/kind-$(shell uname | tr A-Z a-z)-amd64 > ./bin/kind
@chmod +x ./bin/kind

5
NOTICE Normal file
View File

@ -0,0 +1,5 @@
CoreOS Project
Copyright 2014 CoreOS, Inc
This product includes software developed at CoreOS, Inc.
(http://www.coreos.com/).

44
PROPOSAL_TEMPLATE.md Normal file
View File

@ -0,0 +1,44 @@
Proposal
===
(Feel free to change headings here, remove sections that are not relevant, or add other sections)
## Background
Describe what problem is being solved here, and (briefly) how this proposal solves it.
## Data Model
Describe the logical data model that your proposal adds.
## Data Storage
Describe how the data will be persisted.
## API
Describe the methods that the API will expose. If there are any breaking changes be sure to call them out here.
## UI/UX
Is there a front-end component to this work?
## Implementation
Here is where you can go into detail about implementation details like data structures, algorithms, etc.
## Security
What are the security implications of this proposal? How are API requests authenticated? Who can make API calls?
## OIDC/OAUTH2
Does this feature relate to any spec?
## Risks/Alternatives Considered
What are the downsides to this implementation? What other alternatives were considered?
## References
If there's any references or prior art, put that here.

181
README.md
View File

@ -1,143 +1,116 @@
# dex - A federated OpenID Connect provider
# dex
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/dexidp/dex/CI?style=flat-square)
[![Go Report Card](https://goreportcard.com/badge/github.com/dexidp/dex?style=flat-square)](https://goreportcard.com/report/github.com/dexidp/dex)
[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod&style=flat-square)](https://gitpod.io/#https://github.com/dexidp/dex)
![logo](docs/logos/dex-horizontal-color.png)
[![Build Status](https://travis-ci.org/coreos/dex.png?branch=master)](https://travis-ci.org/coreos/dex)
[![Docker Repository on Quay.io](https://quay.io/repository/coreos/dex/status?token=2e772caf-ea17-45d5-8455-8fcf39dae6e1 "Docker Repository on Quay.io")](https://quay.io/repository/coreos/dex)
[![GoDoc](https://godoc.org/github.com/coreos/dex?status.svg)](https://godoc.org/github.com/coreos/dex)
Dex is an identity service that uses [OpenID Connect][openid-connect] to drive authentication for other apps.
dex is a federated identity management service. It provides OpenID Connect (OIDC) and OAuth 2.0 to users, and can proxy to multiple remote identity providers (IdP) to drive actual authentication, as well as managing local username/password credentials.
Dex acts as a portal to other identity providers through ["connectors."](#connectors) This lets dex defer authentication to LDAP servers, SAML providers, or established identity providers like GitHub, Google, and Active Directory. Clients write their authentication logic once to talk to dex, then dex handles the protocols for a given backend.
We named the project 'dex' because it is a central index of users that other pieces of software can authenticate against.
## ID Tokens
ID Tokens are an OAuth2 extension introduced by OpenID Connect and dex's primary feature. ID Tokens are [JSON Web Tokens][jwt-io] (JWTs) signed by dex and returned as part of the OAuth2 response that attest to the end user's identity. An example JWT might look like:
## Architecture
```
eyJhbGciOiJSUzI1NiIsImtpZCI6IjlkNDQ3NDFmNzczYjkzOGNmNjVkZDMyNjY4NWI4NjE4MGMzMjRkOTkifQ.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjU1NTYvZGV4Iiwic3ViIjoiQ2djeU16UXlOelE1RWdabmFYUm9kV0kiLCJhdWQiOiJleGFtcGxlLWFwcCIsImV4cCI6MTQ5Mjg4MjA0MiwiaWF0IjoxNDkyNzk1NjQyLCJhdF9oYXNoIjoiYmk5NmdPWFpTaHZsV1l0YWw5RXFpdyIsImVtYWlsIjoiZXJpYy5jaGlhbmdAY29yZW9zLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJncm91cHMiOlsiYWRtaW5zIiwiZGV2ZWxvcGVycyJdLCJuYW1lIjoiRXJpYyBDaGlhbmcifQ.OhROPq_0eP-zsQRjg87KZ4wGkjiQGnTi5QuG877AdJDb3R2ZCOk2Vkf5SdP8cPyb3VMqL32G4hLDayniiv8f1_ZXAde0sKrayfQ10XAXFgZl_P1yilkLdknxn6nbhDRVllpWcB12ki9vmAxklAr0B1C4kr5nI3-BZLrFcUR5sQbxwJj4oW1OuG6jJCNGHXGNTBTNEaM28eD-9nhfBeuBTzzO7BKwPsojjj4C9ogU4JQhGvm_l4yfVi0boSx8c0FX3JsiB0yLa1ZdJVWVl9m90XmbWRSD85pNDQHcWZP9hR6CMgbvGkZsgjG32qeRwUL_eNkNowSBNWLrGNPoON1gMg
```
dex consists of multiple components:
ID Tokens contains standard claims assert which client app logged the user in, when the token expires, and the identity of the user.
- **dex-worker** is the primary server component of dex
- host a user-facing API that drives the OIDC protocol
- proxy to remote identity providers via "connectors"
- provides an API for administrators to manage users.
- **dex-overlord** is an auxiliary process responsible for various administrative tasks:
- rotation of keys used by the workers to sign identity tokens
- garbage collection of stale data in the database
- provides an API for bootstrapping the system.
- **dexctl** is a CLI tool used to manage a dex deployment
- configure identity provider connectors
- administer OIDC client identities
- **database**; a database is used to for persistent storage for keys, users,
OAuth sessions and other data. Currently Postgres (9.4+) is the only supported
database.
```json
{
"iss": "http://127.0.0.1:5556/dex",
"sub": "CgcyMzQyNzQ5EgZnaXRodWI",
"aud": "example-app",
"exp": 1492882042,
"iat": 1492795642,
"at_hash": "bi96gOXZShvlWYtal9Eqiw",
"email": "jane.doe@coreos.com",
"email_verified": true,
"groups": [
"admins",
"developers"
],
"name": "Jane Doe"
}
```
A typical dex deployment consists of N dex-workers behind a load balancer, and one dex-overlord.
The dex-workers directly handle user requests, so the loss of all workers can result in service downtime.
The single dex-overlord runs its tasks periodically, so it does not need to maintain 100% uptime.
Because these tokens are signed by dex and [contain standard-based claims][standard-claims] other services can consume them as service-to-service credentials. Systems that can already consume OpenID Connect ID Tokens issued by dex include:
## Who Should Use Dex?
* [Kubernetes][kubernetes]
* [AWS STS][aws-sts]
A non-exhaustive list of those who would benefit from using dex:
For details on how to request or validate an ID Token, see [_"Writing apps that use dex"_][using-dex].
- Those who want a language/framework-agnostic way to manage authentication.
- Those who want to federate authentication from multiple providers of differing types.
- Those who want to manage user credentials (e.g. username and password) and perform authentication locally.
- Those who want to create an OIDC Identity Provider for multiple clients to authenticate against.
- Those who want any or all of the above in a Free and Open Source project.
## Kubernetes and Dex
## Getting help with dex
Dex runs natively on top of any Kubernetes cluster using Custom Resource Definitions and can drive API server authentication through the OpenID Connect plugin. Clients, such as the [`kubernetes-dashboard`](https://github.com/kubernetes/dashboard) and `kubectl`, can act on behalf of users who can login to the cluster through any identity provider dex supports.
* More docs for running dex as a Kubernetes authenticator can be found [here](https://dexidp.io/docs/kubernetes/).
* You can find more about companies and projects, which uses dex, [here](./ADOPTERS.md).
* For bugs and feature requests (including documentation!), file an [issue](https://github.com/coreos/dex/issues).
* For general discussion about both using and developing dex, join the [dex-dev](https://groups.google.com/forum/#!forum/dex-dev) mailing list.
* For more details on dex development plans, check out the [roadmap](https://github.com/coreos/dex/blob/master/Documentation/roadmap.md).
## Connectors
When a user logs in through dex, the user's identity is usually stored in another user-management system: a LDAP directory, a GitHub org, etc. Dex acts as a shim between a client app and the upstream identity provider. The client only needs to understand OpenID Connect to query dex, while dex implements an array of protocols for querying other user-management systems.
Remote IdPs could implement any auth-N protocol. *Connectors* contain protocol-specific logic and are used to communicate with remote IdPs. Possible examples of connectors could be: OIDC, LDAP, Local credentials, Basic Auth, etc.
![](docs/img/dex-flow.png)
dex ships with an OIDC connector, useful for authenticating with services like Google and Salesforce (or even other dex instances!) and a "local" connector, in which dex itself presents a UI for users to authenticate via dex-stored credentials.
A "connector" is a strategy used by dex for authenticating a user against another identity provider. Dex implements connectors that target specific platforms such as GitHub, LinkedIn, and Microsoft as well as established protocols like LDAP and SAML.
Future connectors can be developed and added as future interoperability requirements emerge.
Depending on the connectors limitations in protocols can prevent dex from issuing [refresh tokens][scopes] or returning [group membership][scopes] claims. For example, because SAML doesn't provide a non-interactive way to refresh assertions, if a user logs in through the SAML connector dex won't issue a refresh token to its client. Refresh token support is required for clients that require offline access, such as `kubectl`.
## Relevant Specifications
Dex implements the following connectors:
These specs are referenced and implemented to some degree in the `jose` package of this project.
| Name | supports refresh tokens | supports groups claim | supports preferred_username claim | status | notes |
| ---- | ----------------------- | --------------------- | --------------------------------- | ------ | ----- |
| [LDAP](https://dexidp.io/docs/connectors/ldap/) | yes | yes | yes | stable | |
| [GitHub](https://dexidp.io/docs/connectors/github/) | yes | yes | yes | stable | |
| [SAML 2.0](https://dexidp.io/docs/connectors/saml/) | no | yes | no | stable | WARNING: Unmaintained and likely vulnerable to auth bypasses ([#1884](https://github.com/dexidp/dex/discussions/1884)) |
| [GitLab](https://dexidp.io/docs/connectors/gitlab/) | yes | yes | yes | beta | |
| [OpenID Connect](https://dexidp.io/docs/connectors/oidc/) | yes | yes | yes | beta | Includes Salesforce, Azure, etc. |
| [OAuth 2.0](https://dexidp.io/docs/connectors/oauth/) | no | yes | yes | alpha | |
| [Google](https://dexidp.io/docs/connectors/google/) | yes | yes | yes | alpha | |
| [LinkedIn](https://dexidp.io/docs/connectors/linkedin/) | yes | no | no | beta | |
| [Microsoft](https://dexidp.io/docs/connectors/microsoft/) | yes | yes | no | beta | |
| [AuthProxy](https://dexidp.io/docs/connectors/authproxy/) | no | yes | no | alpha | Authentication proxies such as Apache2 mod_auth, etc. |
| [Bitbucket Cloud](https://dexidp.io/docs/connectors/bitbucketcloud/) | yes | yes | no | alpha | |
| [OpenShift](https://dexidp.io/docs/connectors/openshift/) | no | yes | no | alpha | |
| [Atlassian Crowd](https://dexidp.io/docs/connectors/atlassiancrowd/) | yes | yes | yes * | beta | preferred_username claim must be configured through config |
| [Gitea](https://dexidp.io/docs/connectors/gitea/) | yes | no | yes | beta | |
| [OpenStack Keystone](https://dexidp.io/docs/connectors/keystone/) | yes | yes | no | alpha | |
- [JWK](https://tools.ietf.org/html/draft-ietf-jose-json-web-key-36)
- [JWT](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30)
- [JWS](https://tools.ietf.org/html/draft-jones-json-web-signature-04)
Stable, beta, and alpha are defined as:
OpenID Connect (OIDC) is broken up into several specifications. The following (amongst others) are relevant:
* Stable: well tested, in active use, and will not change in backward incompatible ways.
* Beta: tested and unlikely to change in backward incompatible ways.
* Alpha: may be untested by core maintainers and is subject to change in backward incompatible ways.
- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)
- [OpenID Connect Discovery 1.0](https://openid.net/specs/openid-connect-discovery-1_0.html)
- [OAuth 2.0 RFC](https://tools.ietf.org/html/rfc6749)
All changes or deprecations of connector features will be announced in the [release notes][release-notes].
## Example OIDC Discovery Endpoints
## Documentation
- https://accounts.google.com/.well-known/openid-configuration
- https://login.salesforce.com/.well-known/openid-configuration
* [Getting started](https://dexidp.io/docs/getting-started/)
* [Intro to OpenID Connect](https://dexidp.io/docs/openid-connect/)
* [Writing apps that use dex][using-dex]
* [What's new in v2](https://dexidp.io/docs/v2/)
* [Custom scopes, claims, and client features](https://dexidp.io/docs/custom-scopes-claims-clients/)
* [Storage options](https://dexidp.io/docs/storage/)
* [gRPC API](https://dexidp.io/docs/api/)
* [Using Kubernetes with dex](https://dexidp.io/docs/kubernetes/)
* Client libraries
* [Go][go-oidc]
## Next steps
## Reporting a vulnerability
If you want to try out dex quickly with a single process and no database (do *not* run this way in production!) take a look at the [dev guide][dev-guide].
Please see our [security policy](.github/SECURITY.md) for details about reporting vulnerabilities.
For running the full stack check out the [getting started guide][getting-started].
## Getting help
[getting-started]: https://github.com/coreos/dex/blob/master/Documentation/getting-started.md
[dev-guide]: https://github.com/coreos/dex/blob/master/Documentation/dev-guide.md
- For feature requests and bugs, file an [issue](https://github.com/dexidp/dex/issues).
- For general discussion about both using and developing Dex:
- join the [#dexidp](https://cloud-native.slack.com/messages/dexidp) on the CNCF Slack
- open a new [discussion](https://github.com/dexidp/dex/discussions)
- join the [dex-dev](https://groups.google.com/forum/#!forum/dex-dev) mailing list
## Coming Soon
[openid-connect]: https://openid.net/connect/
[standard-claims]: https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
[scopes]: https://dexidp.io/docs/custom-scopes-claims-clients/#scopes
[using-dex]: https://dexidp.io/docs/using-dex/
[jwt-io]: https://jwt.io/
[kubernetes]: http://kubernetes.io/docs/admin/authentication/#openid-connect-tokens
[aws-sts]: https://docs.aws.amazon.com/STS/latest/APIReference/Welcome.html
[go-oidc]: https://github.com/coreos/go-oidc
[issue-1065]: https://github.com/dexidp/dex/issues/1065
[release-notes]: https://github.com/dexidp/dex/releases
- Multiple backing Identity Providers
- Identity Management
- Authorization
## Development
## Similar Software
When all coding and testing is done, please run the test suite:
### [Auth0](https://auth0.com)
```shell
make testall
```
Auth0 is a commercial product which implements the OpenID Connect protocol and [JWT](http://jwt.io). It comes with built-in support for 30+ social providers (and provide extensibility points to add customs); enterprise providers like ADFS, SiteMinder, Ping, Tivoli, or any SAML provider; LDAP/AD connectors that can be run behind firewalls via [an open source agent/connector](https://github.com/auth0/ad-ldap-connector); built-in user/password stores with email and phone verification; legacy user/password stores running Mongo, PG, MySQL, SQL Server among others; multi-factor auth; passwordless support; custom extensibility of the auth pipeline through node.js and many other things.
For the best developer experience, install [Nix](https://builtwithnix.org/) and [direnv](https://direnv.net/).
You could chain dex with Auth0, dex as RP and Auth0 as OpenId Connect Provider, and bring to dex all the providers that come in Auth0 plus the user management capabilities.
Alternatively, install Go and Docker manually or using a package manager. Install the rest of the dependencies by running `make deps`.
### [CloudFoundry UAA](https://github.com/cloudfoundry/uaa)
## License
>The UAA is a multi tenant identity management service, used in Cloud Foundry, but also available as a stand alone OAuth2 server.
The project is licensed under the [Apache License, Version 2.0](LICENSE).
### [OmniAuth](https://github.com/intridea/omniauth)
OmniAuth provides authentication federation at the language (Ruby) level, with a [wide range of integrations](https://github.com/intridea/omniauth/wiki/List-of-Strategies) available.
### [Okta](http://developer.okta.com/product/)
Okta is a commercial product which is similar to dex in that for it too, identity federation is a key feature. It connects to many more authentication providers than dex, and also does the federation in the opposite direction - it can be used as a SSO to other identity providers.
### [Shibboleth](https://shibboleth.net/)
Shibboleth is an open source system implementing the [SAML](https://www.oasis-open.org/standards#samlv2.0) standard, and can federate from a variety of backends, most notably LDAP.

184
admin/api.go Normal file
View File

@ -0,0 +1,184 @@
// Package admin provides an implementation of the API described in auth/schema/adminschema.
package admin
import (
"net/http"
"github.com/coreos/dex/client"
clientmanager "github.com/coreos/dex/client/manager"
"github.com/coreos/dex/connector"
"github.com/coreos/dex/schema/adminschema"
"github.com/coreos/dex/user"
usermanager "github.com/coreos/dex/user/manager"
)
// AdminAPI provides the logic necessary to implement the Admin API.
type AdminAPI struct {
userManager *usermanager.UserManager
userRepo user.UserRepo
passwordInfoRepo user.PasswordInfoRepo
connectorConfigRepo connector.ConnectorConfigRepo
clientRepo client.ClientRepo
clientManager *clientmanager.ClientManager
localConnectorID string
}
func NewAdminAPI(userRepo user.UserRepo, pwiRepo user.PasswordInfoRepo, clientRepo client.ClientRepo, connectorConfigRepo connector.ConnectorConfigRepo, userManager *usermanager.UserManager, clientManager *clientmanager.ClientManager, localConnectorID string) *AdminAPI {
if localConnectorID == "" {
panic("must specify non-blank localConnectorID")
}
return &AdminAPI{
userManager: userManager,
userRepo: userRepo,
passwordInfoRepo: pwiRepo,
clientRepo: clientRepo,
clientManager: clientManager,
connectorConfigRepo: connectorConfigRepo,
localConnectorID: localConnectorID,
}
}
// Error is the error type returned by AdminAPI methods.
type Error struct {
Type string
// The HTTP Code to return for this type of error.
Code int
Desc string
// The underlying error - not to be consumed by external users.
Internal error
}
func (e Error) Error() string {
return e.Type
}
func errorMaker(typ string, desc string, code int) func(internal error) Error {
return func(internal error) Error {
return Error{
Type: typ,
Code: code,
Desc: desc,
Internal: internal,
}
}
}
var (
ErrorMissingClient = errorMaker("bad_request", "The 'client' cannot be empty", http.StatusBadRequest)(nil)
ErrorInvalidClientFunc = errorMaker("bad_request", "Your client could not be validated.", http.StatusBadRequest)
errorMap = map[error]func(error) Error{
client.ErrorMissingRedirectURI: errorMaker("bad_request", "Non-public clients must have at least one redirect URI", http.StatusBadRequest),
client.ErrorPublicClientRedirectURIs: errorMaker("bad_request", "Public clients cannot specify redirect URIs", http.StatusBadRequest),
client.ErrorPublicClientMissingName: errorMaker("bad_request", "Public clients require a ClientName", http.StatusBadRequest),
client.ErrorInvalidClientSecret: errorMaker("bad_request", "Secret must be a base64 encoded string", http.StatusBadRequest),
client.ErrorDuplicateClientID: errorMaker("bad_request", "Client ID already exists.", http.StatusConflict),
user.ErrorNotFound: errorMaker("resource_not_found", "Resource could not be found.", http.StatusNotFound),
user.ErrorDuplicateEmail: errorMaker("bad_request", "Email already in use.", http.StatusConflict),
user.ErrorInvalidEmail: errorMaker("bad_request", "invalid email.", http.StatusBadRequest),
adminschema.ErrorInvalidRedirectURI: errorMaker("bad_request", "invalid redirectURI.", http.StatusBadRequest),
adminschema.ErrorInvalidLogoURI: errorMaker("bad_request", "invalid logoURI.", http.StatusBadRequest),
adminschema.ErrorInvalidClientURI: errorMaker("bad_request", "invalid clientURI.", http.StatusBadRequest),
adminschema.ErrorNoRedirectURI: errorMaker("bad_request", "invalid redirectURI.", http.StatusBadRequest),
}
)
func (a *AdminAPI) GetAdmin(id string) (adminschema.Admin, error) {
usr, err := a.userRepo.Get(nil, id)
if err != nil {
return adminschema.Admin{}, mapError(err)
}
pwi, err := a.passwordInfoRepo.Get(nil, id)
if err != nil {
return adminschema.Admin{}, mapError(err)
}
return adminschema.Admin{
Id: id,
Email: usr.Email,
Password: string(pwi.Password),
}, nil
}
func (a *AdminAPI) CreateAdmin(admn adminschema.Admin) (string, error) {
userID, err := a.userManager.CreateUser(user.User{
Email: admn.Email,
Admin: true}, user.Password(admn.Password), a.localConnectorID)
if err != nil {
return "", mapError(err)
}
return userID, nil
}
func (a *AdminAPI) GetState() (adminschema.State, error) {
state := adminschema.State{}
admins, err := a.userRepo.GetAdminCount(nil)
if err != nil {
return adminschema.State{}, err
}
state.AdminUserCreated = admins > 0
return state, nil
}
func (a *AdminAPI) CreateClient(req adminschema.ClientCreateRequest) (adminschema.ClientCreateResponse, error) {
if req.Client == nil {
return adminschema.ClientCreateResponse{}, ErrorMissingClient
}
cli, err := adminschema.MapSchemaClientToClient(*req.Client)
if err != nil {
return adminschema.ClientCreateResponse{}, mapError(err)
}
creds, err := a.clientManager.New(cli, &clientmanager.ClientOptions{
TrustedPeers: req.Client.TrustedPeers,
})
if err != nil {
return adminschema.ClientCreateResponse{}, mapError(err)
}
req.Client.Id = creds.ID
req.Client.Secret = creds.Secret
return adminschema.ClientCreateResponse{
Client: req.Client,
}, nil
}
func (a *AdminAPI) SetConnectors(connectorConfigs []connector.ConnectorConfig) error {
return a.connectorConfigRepo.Set(connectorConfigs)
}
func (a *AdminAPI) GetConnectors() ([]connector.ConnectorConfig, error) {
return a.connectorConfigRepo.All()
}
func mapError(e error) error {
switch t := e.(type) {
case client.ValidationError:
return ErrorInvalidClientFunc(t)
default:
}
if mapped, ok := errorMap[e]; ok {
return mapped(e)
}
return Error{
Code: http.StatusInternalServerError,
Type: "server_error",
Desc: "",
Internal: e,
}
}

303
admin/api_test.go Normal file
View File

@ -0,0 +1,303 @@
package admin
import (
"testing"
"github.com/coreos/dex/client"
clientmanager "github.com/coreos/dex/client/manager"
"github.com/coreos/dex/connector"
"github.com/coreos/dex/db"
"github.com/coreos/dex/schema/adminschema"
"github.com/coreos/dex/user"
"github.com/coreos/dex/user/manager"
"github.com/kylelemons/godebug/pretty"
)
type testFixtures struct {
ur user.UserRepo
pwr user.PasswordInfoRepo
ccr connector.ConnectorConfigRepo
cr client.ClientRepo
cm *clientmanager.ClientManager
mgr *manager.UserManager
adAPI *AdminAPI
}
func makeTestFixtures() *testFixtures {
f := &testFixtures{}
dbMap := db.NewMemDB()
f.ur = func() user.UserRepo {
repo, err := db.NewUserRepoFromUsers(dbMap, []user.UserWithRemoteIdentities{
{
User: user.User{
ID: "ID-1",
Email: "email-1@example.com",
DisplayName: "Name-1",
},
},
{
User: user.User{
ID: "ID-2",
Email: "email-2@example.com",
DisplayName: "Name-2",
},
},
})
if err != nil {
panic("Failed to create user repo: " + err.Error())
}
return repo
}()
f.pwr = func() user.PasswordInfoRepo {
repo, err := db.NewPasswordInfoRepoFromPasswordInfos(dbMap, []user.PasswordInfo{
{
UserID: "ID-1",
Password: []byte("hi."),
},
})
if err != nil {
panic("Failed to create user repo: " + err.Error())
}
return repo
}()
f.ccr = func() connector.ConnectorConfigRepo {
c := []connector.ConnectorConfig{&connector.LocalConnectorConfig{ID: "local"}}
repo := db.NewConnectorConfigRepo(dbMap)
if err := repo.Set(c); err != nil {
panic(err)
}
return repo
}()
f.mgr = manager.NewUserManager(f.ur, f.pwr, f.ccr, db.TransactionFactory(dbMap), manager.ManagerOptions{})
f.cm = clientmanager.NewClientManager(f.cr, db.TransactionFactory(dbMap), clientmanager.ManagerOptions{})
f.adAPI = NewAdminAPI(f.ur, f.pwr, f.cr, f.ccr, f.mgr, f.cm, "local")
return f
}
func TestGetAdmin(t *testing.T) {
tests := []struct {
id string
wantErr error
}{
{
id: "ID-1",
},
{
// Not found
id: "ID-3",
wantErr: user.ErrorNotFound,
},
}
for i, tt := range tests {
f := makeTestFixtures()
admn, err := f.adAPI.GetAdmin(tt.id)
if tt.wantErr != nil {
if err == nil {
t.Errorf("case %d: err was nil", i)
continue
}
aErr, ok := err.(Error)
if !ok {
t.Errorf("case %d: not an admin.Error: %q", i, err)
continue
}
if aErr.Internal != tt.wantErr {
t.Errorf("case %d: want=%q, got=%q", i, tt.wantErr, aErr.Internal)
continue
}
} else {
if err != nil {
t.Errorf("case %d: err != nil: %q", i, err)
continue
}
if admn.Id != "ID-1" {
t.Errorf("case %d: want=%q, got=%q", i, tt.id, admn.Id)
}
}
}
}
func TestCreateAdmin(t *testing.T) {
hashedPassword, _ := user.NewPasswordFromPlaintext("foopass")
tests := []struct {
admn adminschema.Admin
wantErr error
}{
{
//hashed password
admn: adminschema.Admin{
Email: "goodemail@example.com",
Password: string(hashedPassword),
},
},
{
//plaintext password
admn: adminschema.Admin{
Email: "goodemail@example.com",
Password: "foopass",
},
},
{
// duplicate Email
admn: adminschema.Admin{
Email: "email-2@example.com",
Password: "foopass",
},
wantErr: user.ErrorDuplicateEmail,
},
{
// bad email
admn: adminschema.Admin{
Email: "badEmailexample",
Password: "foopass",
},
wantErr: user.ErrorInvalidEmail,
},
{
// missing Email
admn: adminschema.Admin{
Password: "foopass",
},
wantErr: user.ErrorInvalidEmail,
},
}
for i, tt := range tests {
f := makeTestFixtures()
id, err := f.adAPI.CreateAdmin(tt.admn)
if tt.wantErr != nil {
if err == nil {
t.Errorf("case %d: err was nil", i)
continue
}
aErr, ok := err.(Error)
if !ok {
t.Errorf("case %d: not a admin.Error: %#v", i, err)
continue
}
if aErr.Internal != tt.wantErr {
t.Errorf("case %d: want=%q, got=%q", i, tt.wantErr, aErr.Internal)
continue
}
} else {
if err != nil {
t.Errorf("case %d: err != nil: %q", i, err)
}
gotAdmn, err := f.adAPI.GetAdmin(id)
if err != nil {
t.Errorf("case %d: err != nil: %q", i, err)
}
tt.admn.Id = id
if diff := pretty.Compare(tt.admn, gotAdmn); diff != "" {
t.Errorf("case %d: Compare(want, got) = %v", i, diff)
}
}
}
}
func TestGetState(t *testing.T) {
tests := []struct {
addUsers []user.User
want adminschema.State
}{
{
addUsers: []user.User{
user.User{
ID: "ID-3",
Email: "email-3@example.com",
DisplayName: "Admin",
Admin: true,
},
},
want: adminschema.State{
AdminUserCreated: true,
},
},
{
want: adminschema.State{
AdminUserCreated: false,
},
},
}
for i, tt := range tests {
f := makeTestFixtures()
for _, usr := range tt.addUsers {
_, err := f.mgr.CreateUser(usr, user.Password("foopass"), f.adAPI.localConnectorID)
if err != nil {
t.Fatalf("case %d: err != nil: %q", i, err)
}
}
got, err := f.adAPI.GetState()
if err != nil {
t.Errorf("case %d: err != nil: %q", i, err)
}
if diff := pretty.Compare(tt.want, got); diff != "" {
t.Errorf("case %d: Compare(want, got) = %v", i, diff)
}
}
}
func TestSetConnectors(t *testing.T) {
tests := []struct {
connectors []connector.ConnectorConfig
}{
{
connectors: []connector.ConnectorConfig{
&connector.GitHubConnectorConfig{
ID: "github",
ClientID: "foo",
ClientSecret: "bar",
},
},
},
{
connectors: []connector.ConnectorConfig{
&connector.GitHubConnectorConfig{
ID: "github",
ClientID: "foo",
ClientSecret: "bar",
},
&connector.LocalConnectorConfig{
ID: "local",
},
&connector.BitbucketConnectorConfig{
ID: "bitbucket",
ClientID: "foo",
ClientSecret: "bar",
},
},
},
}
for i, tt := range tests {
f := makeTestFixtures()
if err := f.adAPI.SetConnectors(tt.connectors); err != nil {
t.Errorf("case %d: failed to set connectors: %v", i, err)
continue
}
got, err := f.adAPI.GetConnectors()
if err != nil {
t.Errorf("case %d: failed to get connectors: %v", i, err)
continue
}
if diff := pretty.Compare(tt.connectors, got); diff != "" {
t.Errorf("case %d: Compare(want, got) = %v", i, diff)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,189 +0,0 @@
syntax = "proto3";
package api;
option java_package = "com.coreos.dex.api";
option go_package = "github.com/dexidp/dex/api";
// Client represents an OAuth2 client.
message Client {
string id = 1;
string secret = 2;
repeated string redirect_uris = 3;
repeated string trusted_peers = 4;
bool public = 5;
string name = 6;
string logo_url = 7;
}
// CreateClientReq is a request to make a client.
message CreateClientReq {
Client client = 1;
}
// CreateClientResp returns the response from creating a client.
message CreateClientResp {
bool already_exists = 1;
Client client = 2;
}
// DeleteClientReq is a request to delete a client.
message DeleteClientReq {
// The ID of the client.
string id = 1;
}
// DeleteClientResp determines if the client is deleted successfully.
message DeleteClientResp {
bool not_found = 1;
}
// UpdateClientReq is a request to update an existing client.
message UpdateClientReq {
string id = 1;
repeated string redirect_uris = 2;
repeated string trusted_peers = 3;
string name = 4;
string logo_url = 5;
}
// UpdateClientResp returns the response from updating a client.
message UpdateClientResp {
bool not_found = 1;
}
// TODO(ericchiang): expand this.
// Password is an email for password mapping managed by the storage.
message Password {
string email = 1;
// Currently we do not accept plain text passwords. Could be an option in the future.
bytes hash = 2;
string username = 3;
string user_id = 4;
}
// CreatePasswordReq is a request to make a password.
message CreatePasswordReq {
Password password = 1;
}
// CreatePasswordResp returns the response from creating a password.
message CreatePasswordResp {
bool already_exists = 1;
}
// UpdatePasswordReq is a request to modify an existing password.
message UpdatePasswordReq {
// The email used to lookup the password. This field cannot be modified
string email = 1;
bytes new_hash = 2;
string new_username = 3;
}
// UpdatePasswordResp returns the response from modifying an existing password.
message UpdatePasswordResp {
bool not_found = 1;
}
// DeletePasswordReq is a request to delete a password.
message DeletePasswordReq {
string email = 1;
}
// DeletePasswordResp returns the response from deleting a password.
message DeletePasswordResp {
bool not_found = 1;
}
// ListPasswordReq is a request to enumerate passwords.
message ListPasswordReq {}
// ListPasswordResp returns a list of passwords.
message ListPasswordResp {
repeated Password passwords = 1;
}
// VersionReq is a request to fetch version info.
message VersionReq {}
// VersionResp holds the version info of components.
message VersionResp {
// Semantic version of the server.
string server = 1;
// Numeric version of the API. It increases everytime a new call is added to the API.
// Clients should use this info to determine if the server supports specific features.
int32 api = 2;
}
// RefreshTokenRef contains the metadata for a refresh token that is managed by the storage.
message RefreshTokenRef {
// ID of the refresh token.
string id = 1;
string client_id = 2;
int64 created_at = 5;
int64 last_used = 6;
}
// ListRefreshReq is a request to enumerate the refresh tokens of a user.
message ListRefreshReq {
// The "sub" claim returned in the ID Token.
string user_id = 1;
}
// ListRefreshResp returns a list of refresh tokens for a user.
message ListRefreshResp {
repeated RefreshTokenRef refresh_tokens = 1;
}
// RevokeRefreshReq is a request to revoke the refresh token of the user-client pair.
message RevokeRefreshReq {
// The "sub" claim returned in the ID Token.
string user_id = 1;
string client_id = 2;
}
// RevokeRefreshResp determines if the refresh token is revoked successfully.
message RevokeRefreshResp {
// Set to true is refresh token was not found and token could not be revoked.
bool not_found = 1;
}
message VerifyPasswordReq {
string email = 1;
string password = 2;
}
message VerifyPasswordResp {
bool verified = 1;
bool not_found = 2;
}
// Dex represents the dex gRPC service.
service Dex {
// CreateClient creates a client.
rpc CreateClient(CreateClientReq) returns (CreateClientResp) {};
// UpdateClient updates an existing client
rpc UpdateClient(UpdateClientReq) returns (UpdateClientResp) {};
// DeleteClient deletes the provided client.
rpc DeleteClient(DeleteClientReq) returns (DeleteClientResp) {};
// CreatePassword creates a password.
rpc CreatePassword(CreatePasswordReq) returns (CreatePasswordResp) {};
// UpdatePassword modifies existing password.
rpc UpdatePassword(UpdatePasswordReq) returns (UpdatePasswordResp) {};
// DeletePassword deletes the password.
rpc DeletePassword(DeletePasswordReq) returns (DeletePasswordResp) {};
// ListPassword lists all password entries.
rpc ListPasswords(ListPasswordReq) returns (ListPasswordResp) {};
// GetVersion returns version information of the server.
rpc GetVersion(VersionReq) returns (VersionResp) {};
// ListRefresh lists all the refresh token entries for a particular user.
rpc ListRefresh(ListRefreshReq) returns (ListRefreshResp) {};
// RevokeRefresh revokes the refresh token for the provided user-client pair.
//
// Note that each user-client pair can have only one refresh token at a time.
rpc RevokeRefresh(RevokeRefreshReq) returns (RevokeRefreshResp) {};
// VerifyPassword returns whether a password matches a hash for a specific email or not.
rpc VerifyPassword(VerifyPasswordReq) returns (VerifyPasswordResp) {};
}

View File

@ -1,487 +0,0 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
package api
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// DexClient is the client API for Dex service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type DexClient interface {
// CreateClient creates a client.
CreateClient(ctx context.Context, in *CreateClientReq, opts ...grpc.CallOption) (*CreateClientResp, error)
// UpdateClient updates an existing client
UpdateClient(ctx context.Context, in *UpdateClientReq, opts ...grpc.CallOption) (*UpdateClientResp, error)
// DeleteClient deletes the provided client.
DeleteClient(ctx context.Context, in *DeleteClientReq, opts ...grpc.CallOption) (*DeleteClientResp, error)
// CreatePassword creates a password.
CreatePassword(ctx context.Context, in *CreatePasswordReq, opts ...grpc.CallOption) (*CreatePasswordResp, error)
// UpdatePassword modifies existing password.
UpdatePassword(ctx context.Context, in *UpdatePasswordReq, opts ...grpc.CallOption) (*UpdatePasswordResp, error)
// DeletePassword deletes the password.
DeletePassword(ctx context.Context, in *DeletePasswordReq, opts ...grpc.CallOption) (*DeletePasswordResp, error)
// ListPassword lists all password entries.
ListPasswords(ctx context.Context, in *ListPasswordReq, opts ...grpc.CallOption) (*ListPasswordResp, error)
// GetVersion returns version information of the server.
GetVersion(ctx context.Context, in *VersionReq, opts ...grpc.CallOption) (*VersionResp, error)
// ListRefresh lists all the refresh token entries for a particular user.
ListRefresh(ctx context.Context, in *ListRefreshReq, opts ...grpc.CallOption) (*ListRefreshResp, error)
// RevokeRefresh revokes the refresh token for the provided user-client pair.
//
// Note that each user-client pair can have only one refresh token at a time.
RevokeRefresh(ctx context.Context, in *RevokeRefreshReq, opts ...grpc.CallOption) (*RevokeRefreshResp, error)
// VerifyPassword returns whether a password matches a hash for a specific email or not.
VerifyPassword(ctx context.Context, in *VerifyPasswordReq, opts ...grpc.CallOption) (*VerifyPasswordResp, error)
}
type dexClient struct {
cc grpc.ClientConnInterface
}
func NewDexClient(cc grpc.ClientConnInterface) DexClient {
return &dexClient{cc}
}
func (c *dexClient) CreateClient(ctx context.Context, in *CreateClientReq, opts ...grpc.CallOption) (*CreateClientResp, error) {
out := new(CreateClientResp)
err := c.cc.Invoke(ctx, "/api.Dex/CreateClient", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dexClient) UpdateClient(ctx context.Context, in *UpdateClientReq, opts ...grpc.CallOption) (*UpdateClientResp, error) {
out := new(UpdateClientResp)
err := c.cc.Invoke(ctx, "/api.Dex/UpdateClient", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dexClient) DeleteClient(ctx context.Context, in *DeleteClientReq, opts ...grpc.CallOption) (*DeleteClientResp, error) {
out := new(DeleteClientResp)
err := c.cc.Invoke(ctx, "/api.Dex/DeleteClient", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dexClient) CreatePassword(ctx context.Context, in *CreatePasswordReq, opts ...grpc.CallOption) (*CreatePasswordResp, error) {
out := new(CreatePasswordResp)
err := c.cc.Invoke(ctx, "/api.Dex/CreatePassword", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dexClient) UpdatePassword(ctx context.Context, in *UpdatePasswordReq, opts ...grpc.CallOption) (*UpdatePasswordResp, error) {
out := new(UpdatePasswordResp)
err := c.cc.Invoke(ctx, "/api.Dex/UpdatePassword", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dexClient) DeletePassword(ctx context.Context, in *DeletePasswordReq, opts ...grpc.CallOption) (*DeletePasswordResp, error) {
out := new(DeletePasswordResp)
err := c.cc.Invoke(ctx, "/api.Dex/DeletePassword", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dexClient) ListPasswords(ctx context.Context, in *ListPasswordReq, opts ...grpc.CallOption) (*ListPasswordResp, error) {
out := new(ListPasswordResp)
err := c.cc.Invoke(ctx, "/api.Dex/ListPasswords", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dexClient) GetVersion(ctx context.Context, in *VersionReq, opts ...grpc.CallOption) (*VersionResp, error) {
out := new(VersionResp)
err := c.cc.Invoke(ctx, "/api.Dex/GetVersion", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dexClient) ListRefresh(ctx context.Context, in *ListRefreshReq, opts ...grpc.CallOption) (*ListRefreshResp, error) {
out := new(ListRefreshResp)
err := c.cc.Invoke(ctx, "/api.Dex/ListRefresh", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dexClient) RevokeRefresh(ctx context.Context, in *RevokeRefreshReq, opts ...grpc.CallOption) (*RevokeRefreshResp, error) {
out := new(RevokeRefreshResp)
err := c.cc.Invoke(ctx, "/api.Dex/RevokeRefresh", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dexClient) VerifyPassword(ctx context.Context, in *VerifyPasswordReq, opts ...grpc.CallOption) (*VerifyPasswordResp, error) {
out := new(VerifyPasswordResp)
err := c.cc.Invoke(ctx, "/api.Dex/VerifyPassword", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// DexServer is the server API for Dex service.
// All implementations must embed UnimplementedDexServer
// for forward compatibility
type DexServer interface {
// CreateClient creates a client.
CreateClient(context.Context, *CreateClientReq) (*CreateClientResp, error)
// UpdateClient updates an existing client
UpdateClient(context.Context, *UpdateClientReq) (*UpdateClientResp, error)
// DeleteClient deletes the provided client.
DeleteClient(context.Context, *DeleteClientReq) (*DeleteClientResp, error)
// CreatePassword creates a password.
CreatePassword(context.Context, *CreatePasswordReq) (*CreatePasswordResp, error)
// UpdatePassword modifies existing password.
UpdatePassword(context.Context, *UpdatePasswordReq) (*UpdatePasswordResp, error)
// DeletePassword deletes the password.
DeletePassword(context.Context, *DeletePasswordReq) (*DeletePasswordResp, error)
// ListPassword lists all password entries.
ListPasswords(context.Context, *ListPasswordReq) (*ListPasswordResp, error)
// GetVersion returns version information of the server.
GetVersion(context.Context, *VersionReq) (*VersionResp, error)
// ListRefresh lists all the refresh token entries for a particular user.
ListRefresh(context.Context, *ListRefreshReq) (*ListRefreshResp, error)
// RevokeRefresh revokes the refresh token for the provided user-client pair.
//
// Note that each user-client pair can have only one refresh token at a time.
RevokeRefresh(context.Context, *RevokeRefreshReq) (*RevokeRefreshResp, error)
// VerifyPassword returns whether a password matches a hash for a specific email or not.
VerifyPassword(context.Context, *VerifyPasswordReq) (*VerifyPasswordResp, error)
mustEmbedUnimplementedDexServer()
}
// UnimplementedDexServer must be embedded to have forward compatible implementations.
type UnimplementedDexServer struct {
}
func (UnimplementedDexServer) CreateClient(context.Context, *CreateClientReq) (*CreateClientResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateClient not implemented")
}
func (UnimplementedDexServer) UpdateClient(context.Context, *UpdateClientReq) (*UpdateClientResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateClient not implemented")
}
func (UnimplementedDexServer) DeleteClient(context.Context, *DeleteClientReq) (*DeleteClientResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteClient not implemented")
}
func (UnimplementedDexServer) CreatePassword(context.Context, *CreatePasswordReq) (*CreatePasswordResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreatePassword not implemented")
}
func (UnimplementedDexServer) UpdatePassword(context.Context, *UpdatePasswordReq) (*UpdatePasswordResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdatePassword not implemented")
}
func (UnimplementedDexServer) DeletePassword(context.Context, *DeletePasswordReq) (*DeletePasswordResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeletePassword not implemented")
}
func (UnimplementedDexServer) ListPasswords(context.Context, *ListPasswordReq) (*ListPasswordResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListPasswords not implemented")
}
func (UnimplementedDexServer) GetVersion(context.Context, *VersionReq) (*VersionResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetVersion not implemented")
}
func (UnimplementedDexServer) ListRefresh(context.Context, *ListRefreshReq) (*ListRefreshResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListRefresh not implemented")
}
func (UnimplementedDexServer) RevokeRefresh(context.Context, *RevokeRefreshReq) (*RevokeRefreshResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method RevokeRefresh not implemented")
}
func (UnimplementedDexServer) VerifyPassword(context.Context, *VerifyPasswordReq) (*VerifyPasswordResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method VerifyPassword not implemented")
}
func (UnimplementedDexServer) mustEmbedUnimplementedDexServer() {}
// UnsafeDexServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to DexServer will
// result in compilation errors.
type UnsafeDexServer interface {
mustEmbedUnimplementedDexServer()
}
func RegisterDexServer(s grpc.ServiceRegistrar, srv DexServer) {
s.RegisterService(&Dex_ServiceDesc, srv)
}
func _Dex_CreateClient_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateClientReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).CreateClient(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/CreateClient",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).CreateClient(ctx, req.(*CreateClientReq))
}
return interceptor(ctx, in, info, handler)
}
func _Dex_UpdateClient_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateClientReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).UpdateClient(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/UpdateClient",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).UpdateClient(ctx, req.(*UpdateClientReq))
}
return interceptor(ctx, in, info, handler)
}
func _Dex_DeleteClient_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteClientReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).DeleteClient(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/DeleteClient",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).DeleteClient(ctx, req.(*DeleteClientReq))
}
return interceptor(ctx, in, info, handler)
}
func _Dex_CreatePassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreatePasswordReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).CreatePassword(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/CreatePassword",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).CreatePassword(ctx, req.(*CreatePasswordReq))
}
return interceptor(ctx, in, info, handler)
}
func _Dex_UpdatePassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdatePasswordReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).UpdatePassword(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/UpdatePassword",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).UpdatePassword(ctx, req.(*UpdatePasswordReq))
}
return interceptor(ctx, in, info, handler)
}
func _Dex_DeletePassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeletePasswordReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).DeletePassword(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/DeletePassword",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).DeletePassword(ctx, req.(*DeletePasswordReq))
}
return interceptor(ctx, in, info, handler)
}
func _Dex_ListPasswords_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListPasswordReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).ListPasswords(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/ListPasswords",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).ListPasswords(ctx, req.(*ListPasswordReq))
}
return interceptor(ctx, in, info, handler)
}
func _Dex_GetVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(VersionReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).GetVersion(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/GetVersion",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).GetVersion(ctx, req.(*VersionReq))
}
return interceptor(ctx, in, info, handler)
}
func _Dex_ListRefresh_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListRefreshReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).ListRefresh(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/ListRefresh",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).ListRefresh(ctx, req.(*ListRefreshReq))
}
return interceptor(ctx, in, info, handler)
}
func _Dex_RevokeRefresh_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RevokeRefreshReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).RevokeRefresh(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/RevokeRefresh",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).RevokeRefresh(ctx, req.(*RevokeRefreshReq))
}
return interceptor(ctx, in, info, handler)
}
func _Dex_VerifyPassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(VerifyPasswordReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).VerifyPassword(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/VerifyPassword",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).VerifyPassword(ctx, req.(*VerifyPasswordReq))
}
return interceptor(ctx, in, info, handler)
}
// Dex_ServiceDesc is the grpc.ServiceDesc for Dex service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Dex_ServiceDesc = grpc.ServiceDesc{
ServiceName: "api.Dex",
HandlerType: (*DexServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "CreateClient",
Handler: _Dex_CreateClient_Handler,
},
{
MethodName: "UpdateClient",
Handler: _Dex_UpdateClient_Handler,
},
{
MethodName: "DeleteClient",
Handler: _Dex_DeleteClient_Handler,
},
{
MethodName: "CreatePassword",
Handler: _Dex_CreatePassword_Handler,
},
{
MethodName: "UpdatePassword",
Handler: _Dex_UpdatePassword_Handler,
},
{
MethodName: "DeletePassword",
Handler: _Dex_DeletePassword_Handler,
},
{
MethodName: "ListPasswords",
Handler: _Dex_ListPasswords_Handler,
},
{
MethodName: "GetVersion",
Handler: _Dex_GetVersion_Handler,
},
{
MethodName: "ListRefresh",
Handler: _Dex_ListRefresh_Handler,
},
{
MethodName: "RevokeRefresh",
Handler: _Dex_RevokeRefresh_Handler,
},
{
MethodName: "VerifyPassword",
Handler: _Dex_VerifyPassword_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "api/api.proto",
}

File diff suppressed because it is too large Load Diff

View File

@ -1,189 +0,0 @@
syntax = "proto3";
package api;
option java_package = "com.coreos.dex.api";
option go_package = "github.com/dexidp/dex/api/v2;api";
// Client represents an OAuth2 client.
message Client {
string id = 1;
string secret = 2;
repeated string redirect_uris = 3;
repeated string trusted_peers = 4;
bool public = 5;
string name = 6;
string logo_url = 7;
}
// CreateClientReq is a request to make a client.
message CreateClientReq {
Client client = 1;
}
// CreateClientResp returns the response from creating a client.
message CreateClientResp {
bool already_exists = 1;
Client client = 2;
}
// DeleteClientReq is a request to delete a client.
message DeleteClientReq {
// The ID of the client.
string id = 1;
}
// DeleteClientResp determines if the client is deleted successfully.
message DeleteClientResp {
bool not_found = 1;
}
// UpdateClientReq is a request to update an existing client.
message UpdateClientReq {
string id = 1;
repeated string redirect_uris = 2;
repeated string trusted_peers = 3;
string name = 4;
string logo_url = 5;
}
// UpdateClientResp returns the response from updating a client.
message UpdateClientResp {
bool not_found = 1;
}
// TODO(ericchiang): expand this.
// Password is an email for password mapping managed by the storage.
message Password {
string email = 1;
// Currently we do not accept plain text passwords. Could be an option in the future.
bytes hash = 2;
string username = 3;
string user_id = 4;
}
// CreatePasswordReq is a request to make a password.
message CreatePasswordReq {
Password password = 1;
}
// CreatePasswordResp returns the response from creating a password.
message CreatePasswordResp {
bool already_exists = 1;
}
// UpdatePasswordReq is a request to modify an existing password.
message UpdatePasswordReq {
// The email used to lookup the password. This field cannot be modified
string email = 1;
bytes new_hash = 2;
string new_username = 3;
}
// UpdatePasswordResp returns the response from modifying an existing password.
message UpdatePasswordResp {
bool not_found = 1;
}
// DeletePasswordReq is a request to delete a password.
message DeletePasswordReq {
string email = 1;
}
// DeletePasswordResp returns the response from deleting a password.
message DeletePasswordResp {
bool not_found = 1;
}
// ListPasswordReq is a request to enumerate passwords.
message ListPasswordReq {}
// ListPasswordResp returns a list of passwords.
message ListPasswordResp {
repeated Password passwords = 1;
}
// VersionReq is a request to fetch version info.
message VersionReq {}
// VersionResp holds the version info of components.
message VersionResp {
// Semantic version of the server.
string server = 1;
// Numeric version of the API. It increases everytime a new call is added to the API.
// Clients should use this info to determine if the server supports specific features.
int32 api = 2;
}
// RefreshTokenRef contains the metadata for a refresh token that is managed by the storage.
message RefreshTokenRef {
// ID of the refresh token.
string id = 1;
string client_id = 2;
int64 created_at = 5;
int64 last_used = 6;
}
// ListRefreshReq is a request to enumerate the refresh tokens of a user.
message ListRefreshReq {
// The "sub" claim returned in the ID Token.
string user_id = 1;
}
// ListRefreshResp returns a list of refresh tokens for a user.
message ListRefreshResp {
repeated RefreshTokenRef refresh_tokens = 1;
}
// RevokeRefreshReq is a request to revoke the refresh token of the user-client pair.
message RevokeRefreshReq {
// The "sub" claim returned in the ID Token.
string user_id = 1;
string client_id = 2;
}
// RevokeRefreshResp determines if the refresh token is revoked successfully.
message RevokeRefreshResp {
// Set to true is refresh token was not found and token could not be revoked.
bool not_found = 1;
}
message VerifyPasswordReq {
string email = 1;
string password = 2;
}
message VerifyPasswordResp {
bool verified = 1;
bool not_found = 2;
}
// Dex represents the dex gRPC service.
service Dex {
// CreateClient creates a client.
rpc CreateClient(CreateClientReq) returns (CreateClientResp) {};
// UpdateClient updates an existing client
rpc UpdateClient(UpdateClientReq) returns (UpdateClientResp) {};
// DeleteClient deletes the provided client.
rpc DeleteClient(DeleteClientReq) returns (DeleteClientResp) {};
// CreatePassword creates a password.
rpc CreatePassword(CreatePasswordReq) returns (CreatePasswordResp) {};
// UpdatePassword modifies existing password.
rpc UpdatePassword(UpdatePasswordReq) returns (UpdatePasswordResp) {};
// DeletePassword deletes the password.
rpc DeletePassword(DeletePasswordReq) returns (DeletePasswordResp) {};
// ListPassword lists all password entries.
rpc ListPasswords(ListPasswordReq) returns (ListPasswordResp) {};
// GetVersion returns version information of the server.
rpc GetVersion(VersionReq) returns (VersionResp) {};
// ListRefresh lists all the refresh token entries for a particular user.
rpc ListRefresh(ListRefreshReq) returns (ListRefreshResp) {};
// RevokeRefresh revokes the refresh token for the provided user-client pair.
//
// Note that each user-client pair can have only one refresh token at a time.
rpc RevokeRefresh(RevokeRefreshReq) returns (RevokeRefreshResp) {};
// VerifyPassword returns whether a password matches a hash for a specific email or not.
rpc VerifyPassword(VerifyPasswordReq) returns (VerifyPasswordResp) {};
}

View File

@ -1,487 +0,0 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
package api
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// DexClient is the client API for Dex service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type DexClient interface {
// CreateClient creates a client.
CreateClient(ctx context.Context, in *CreateClientReq, opts ...grpc.CallOption) (*CreateClientResp, error)
// UpdateClient updates an existing client
UpdateClient(ctx context.Context, in *UpdateClientReq, opts ...grpc.CallOption) (*UpdateClientResp, error)
// DeleteClient deletes the provided client.
DeleteClient(ctx context.Context, in *DeleteClientReq, opts ...grpc.CallOption) (*DeleteClientResp, error)
// CreatePassword creates a password.
CreatePassword(ctx context.Context, in *CreatePasswordReq, opts ...grpc.CallOption) (*CreatePasswordResp, error)
// UpdatePassword modifies existing password.
UpdatePassword(ctx context.Context, in *UpdatePasswordReq, opts ...grpc.CallOption) (*UpdatePasswordResp, error)
// DeletePassword deletes the password.
DeletePassword(ctx context.Context, in *DeletePasswordReq, opts ...grpc.CallOption) (*DeletePasswordResp, error)
// ListPassword lists all password entries.
ListPasswords(ctx context.Context, in *ListPasswordReq, opts ...grpc.CallOption) (*ListPasswordResp, error)
// GetVersion returns version information of the server.
GetVersion(ctx context.Context, in *VersionReq, opts ...grpc.CallOption) (*VersionResp, error)
// ListRefresh lists all the refresh token entries for a particular user.
ListRefresh(ctx context.Context, in *ListRefreshReq, opts ...grpc.CallOption) (*ListRefreshResp, error)
// RevokeRefresh revokes the refresh token for the provided user-client pair.
//
// Note that each user-client pair can have only one refresh token at a time.
RevokeRefresh(ctx context.Context, in *RevokeRefreshReq, opts ...grpc.CallOption) (*RevokeRefreshResp, error)
// VerifyPassword returns whether a password matches a hash for a specific email or not.
VerifyPassword(ctx context.Context, in *VerifyPasswordReq, opts ...grpc.CallOption) (*VerifyPasswordResp, error)
}
type dexClient struct {
cc grpc.ClientConnInterface
}
func NewDexClient(cc grpc.ClientConnInterface) DexClient {
return &dexClient{cc}
}
func (c *dexClient) CreateClient(ctx context.Context, in *CreateClientReq, opts ...grpc.CallOption) (*CreateClientResp, error) {
out := new(CreateClientResp)
err := c.cc.Invoke(ctx, "/api.Dex/CreateClient", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dexClient) UpdateClient(ctx context.Context, in *UpdateClientReq, opts ...grpc.CallOption) (*UpdateClientResp, error) {
out := new(UpdateClientResp)
err := c.cc.Invoke(ctx, "/api.Dex/UpdateClient", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dexClient) DeleteClient(ctx context.Context, in *DeleteClientReq, opts ...grpc.CallOption) (*DeleteClientResp, error) {
out := new(DeleteClientResp)
err := c.cc.Invoke(ctx, "/api.Dex/DeleteClient", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dexClient) CreatePassword(ctx context.Context, in *CreatePasswordReq, opts ...grpc.CallOption) (*CreatePasswordResp, error) {
out := new(CreatePasswordResp)
err := c.cc.Invoke(ctx, "/api.Dex/CreatePassword", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dexClient) UpdatePassword(ctx context.Context, in *UpdatePasswordReq, opts ...grpc.CallOption) (*UpdatePasswordResp, error) {
out := new(UpdatePasswordResp)
err := c.cc.Invoke(ctx, "/api.Dex/UpdatePassword", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dexClient) DeletePassword(ctx context.Context, in *DeletePasswordReq, opts ...grpc.CallOption) (*DeletePasswordResp, error) {
out := new(DeletePasswordResp)
err := c.cc.Invoke(ctx, "/api.Dex/DeletePassword", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dexClient) ListPasswords(ctx context.Context, in *ListPasswordReq, opts ...grpc.CallOption) (*ListPasswordResp, error) {
out := new(ListPasswordResp)
err := c.cc.Invoke(ctx, "/api.Dex/ListPasswords", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dexClient) GetVersion(ctx context.Context, in *VersionReq, opts ...grpc.CallOption) (*VersionResp, error) {
out := new(VersionResp)
err := c.cc.Invoke(ctx, "/api.Dex/GetVersion", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dexClient) ListRefresh(ctx context.Context, in *ListRefreshReq, opts ...grpc.CallOption) (*ListRefreshResp, error) {
out := new(ListRefreshResp)
err := c.cc.Invoke(ctx, "/api.Dex/ListRefresh", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dexClient) RevokeRefresh(ctx context.Context, in *RevokeRefreshReq, opts ...grpc.CallOption) (*RevokeRefreshResp, error) {
out := new(RevokeRefreshResp)
err := c.cc.Invoke(ctx, "/api.Dex/RevokeRefresh", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dexClient) VerifyPassword(ctx context.Context, in *VerifyPasswordReq, opts ...grpc.CallOption) (*VerifyPasswordResp, error) {
out := new(VerifyPasswordResp)
err := c.cc.Invoke(ctx, "/api.Dex/VerifyPassword", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// DexServer is the server API for Dex service.
// All implementations must embed UnimplementedDexServer
// for forward compatibility
type DexServer interface {
// CreateClient creates a client.
CreateClient(context.Context, *CreateClientReq) (*CreateClientResp, error)
// UpdateClient updates an existing client
UpdateClient(context.Context, *UpdateClientReq) (*UpdateClientResp, error)
// DeleteClient deletes the provided client.
DeleteClient(context.Context, *DeleteClientReq) (*DeleteClientResp, error)
// CreatePassword creates a password.
CreatePassword(context.Context, *CreatePasswordReq) (*CreatePasswordResp, error)
// UpdatePassword modifies existing password.
UpdatePassword(context.Context, *UpdatePasswordReq) (*UpdatePasswordResp, error)
// DeletePassword deletes the password.
DeletePassword(context.Context, *DeletePasswordReq) (*DeletePasswordResp, error)
// ListPassword lists all password entries.
ListPasswords(context.Context, *ListPasswordReq) (*ListPasswordResp, error)
// GetVersion returns version information of the server.
GetVersion(context.Context, *VersionReq) (*VersionResp, error)
// ListRefresh lists all the refresh token entries for a particular user.
ListRefresh(context.Context, *ListRefreshReq) (*ListRefreshResp, error)
// RevokeRefresh revokes the refresh token for the provided user-client pair.
//
// Note that each user-client pair can have only one refresh token at a time.
RevokeRefresh(context.Context, *RevokeRefreshReq) (*RevokeRefreshResp, error)
// VerifyPassword returns whether a password matches a hash for a specific email or not.
VerifyPassword(context.Context, *VerifyPasswordReq) (*VerifyPasswordResp, error)
mustEmbedUnimplementedDexServer()
}
// UnimplementedDexServer must be embedded to have forward compatible implementations.
type UnimplementedDexServer struct {
}
func (UnimplementedDexServer) CreateClient(context.Context, *CreateClientReq) (*CreateClientResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateClient not implemented")
}
func (UnimplementedDexServer) UpdateClient(context.Context, *UpdateClientReq) (*UpdateClientResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateClient not implemented")
}
func (UnimplementedDexServer) DeleteClient(context.Context, *DeleteClientReq) (*DeleteClientResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteClient not implemented")
}
func (UnimplementedDexServer) CreatePassword(context.Context, *CreatePasswordReq) (*CreatePasswordResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreatePassword not implemented")
}
func (UnimplementedDexServer) UpdatePassword(context.Context, *UpdatePasswordReq) (*UpdatePasswordResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdatePassword not implemented")
}
func (UnimplementedDexServer) DeletePassword(context.Context, *DeletePasswordReq) (*DeletePasswordResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeletePassword not implemented")
}
func (UnimplementedDexServer) ListPasswords(context.Context, *ListPasswordReq) (*ListPasswordResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListPasswords not implemented")
}
func (UnimplementedDexServer) GetVersion(context.Context, *VersionReq) (*VersionResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetVersion not implemented")
}
func (UnimplementedDexServer) ListRefresh(context.Context, *ListRefreshReq) (*ListRefreshResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListRefresh not implemented")
}
func (UnimplementedDexServer) RevokeRefresh(context.Context, *RevokeRefreshReq) (*RevokeRefreshResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method RevokeRefresh not implemented")
}
func (UnimplementedDexServer) VerifyPassword(context.Context, *VerifyPasswordReq) (*VerifyPasswordResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method VerifyPassword not implemented")
}
func (UnimplementedDexServer) mustEmbedUnimplementedDexServer() {}
// UnsafeDexServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to DexServer will
// result in compilation errors.
type UnsafeDexServer interface {
mustEmbedUnimplementedDexServer()
}
func RegisterDexServer(s grpc.ServiceRegistrar, srv DexServer) {
s.RegisterService(&Dex_ServiceDesc, srv)
}
func _Dex_CreateClient_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateClientReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).CreateClient(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/CreateClient",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).CreateClient(ctx, req.(*CreateClientReq))
}
return interceptor(ctx, in, info, handler)
}
func _Dex_UpdateClient_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateClientReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).UpdateClient(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/UpdateClient",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).UpdateClient(ctx, req.(*UpdateClientReq))
}
return interceptor(ctx, in, info, handler)
}
func _Dex_DeleteClient_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteClientReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).DeleteClient(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/DeleteClient",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).DeleteClient(ctx, req.(*DeleteClientReq))
}
return interceptor(ctx, in, info, handler)
}
func _Dex_CreatePassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreatePasswordReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).CreatePassword(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/CreatePassword",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).CreatePassword(ctx, req.(*CreatePasswordReq))
}
return interceptor(ctx, in, info, handler)
}
func _Dex_UpdatePassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdatePasswordReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).UpdatePassword(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/UpdatePassword",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).UpdatePassword(ctx, req.(*UpdatePasswordReq))
}
return interceptor(ctx, in, info, handler)
}
func _Dex_DeletePassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeletePasswordReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).DeletePassword(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/DeletePassword",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).DeletePassword(ctx, req.(*DeletePasswordReq))
}
return interceptor(ctx, in, info, handler)
}
func _Dex_ListPasswords_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListPasswordReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).ListPasswords(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/ListPasswords",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).ListPasswords(ctx, req.(*ListPasswordReq))
}
return interceptor(ctx, in, info, handler)
}
func _Dex_GetVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(VersionReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).GetVersion(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/GetVersion",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).GetVersion(ctx, req.(*VersionReq))
}
return interceptor(ctx, in, info, handler)
}
func _Dex_ListRefresh_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListRefreshReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).ListRefresh(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/ListRefresh",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).ListRefresh(ctx, req.(*ListRefreshReq))
}
return interceptor(ctx, in, info, handler)
}
func _Dex_RevokeRefresh_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RevokeRefreshReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).RevokeRefresh(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/RevokeRefresh",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).RevokeRefresh(ctx, req.(*RevokeRefreshReq))
}
return interceptor(ctx, in, info, handler)
}
func _Dex_VerifyPassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(VerifyPasswordReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DexServer).VerifyPassword(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.Dex/VerifyPassword",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DexServer).VerifyPassword(ctx, req.(*VerifyPasswordReq))
}
return interceptor(ctx, in, info, handler)
}
// Dex_ServiceDesc is the grpc.ServiceDesc for Dex service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Dex_ServiceDesc = grpc.ServiceDesc{
ServiceName: "api.Dex",
HandlerType: (*DexServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "CreateClient",
Handler: _Dex_CreateClient_Handler,
},
{
MethodName: "UpdateClient",
Handler: _Dex_UpdateClient_Handler,
},
{
MethodName: "DeleteClient",
Handler: _Dex_DeleteClient_Handler,
},
{
MethodName: "CreatePassword",
Handler: _Dex_CreatePassword_Handler,
},
{
MethodName: "UpdatePassword",
Handler: _Dex_UpdatePassword_Handler,
},
{
MethodName: "DeletePassword",
Handler: _Dex_DeletePassword_Handler,
},
{
MethodName: "ListPasswords",
Handler: _Dex_ListPasswords_Handler,
},
{
MethodName: "GetVersion",
Handler: _Dex_GetVersion_Handler,
},
{
MethodName: "ListRefresh",
Handler: _Dex_ListRefresh_Handler,
},
{
MethodName: "RevokeRefresh",
Handler: _Dex_RevokeRefresh_Handler,
},
{
MethodName: "VerifyPassword",
Handler: _Dex_VerifyPassword_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "api/v2/api.proto",
}

View File

@ -1,16 +0,0 @@
module github.com/dexidp/dex/api/v2
go 1.17
require (
google.golang.org/grpc v1.47.0
google.golang.org/protobuf v1.28.0
)
require (
github.com/golang/protobuf v1.5.2 // indirect
golang.org/x/net v0.0.0-20220607020251-c690dde0001d // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8 // indirect
)

View File

@ -1,141 +0,0 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8 h1:qRu95HZ148xXw+XeZ3dvqe85PxH4X8+jIo0iRPKcEnM=
google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8=
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

19
build Executable file
View File

@ -0,0 +1,19 @@
#!/bin/bash -e
source ./env
CMDS=( "dex-worker" "dexctl" "dex-overlord" "gendoc")
FORMAT='{{ range $i, $dep := .Deps }}{{ $dep }} {{ end }}'
for CMD in ${CMDS[@]}; do
TARGET="github.com/coreos/dex/cmd/$CMD"
# Install command dependencies. This caches package builds and speeds
# up successive builds a lot.
go list -f="$FORMAT" $TARGET | xargs go install -ldflags="$LD_FLAGS"
# Build the actual command.
go build -o="bin/$CMD" -ldflags="$LD_FLAGS" $TARGET
done
go build -o bin/example-app github.com/coreos/dex/examples/app
go build -o bin/example-cli github.com/coreos/dex/examples/cli

51
build-docker Executable file
View File

@ -0,0 +1,51 @@
#!/bin/bash -e
#
# Build the docker container and push it to quay
# ./build-docker push
#
# Build the docker container locally without pushing to registry
# ./build-docker build
DOCKER_REGISTRY=${DOCKER_REGISTRY:=quay.io}
DOCKER_REPO=${DOCKER_REPO:=coreos/dex}
repo=$DOCKER_REGISTRY/$DOCKER_REPO
version=$(./git-version)
stage() {
echo -e "\033[36m----> $1\033[0m"
}
build() {
stage "build $repo:$version"
docker build -q --rm=true -t $repo:$version .
docker tag $repo:$version $repo:latest
}
push() {
stage "push image $repo:$version"
if [ -v $DOCKER_USER ] || [ -v $DOCKER_PASSWORD ]; then
echo "env variables not set: DOCKER_USER, DOCKER_PASSWORD. skipping login, assuming creds in .dockercfg"
else
echo logging in as $DOCKER_USER
docker login --username="$DOCKER_USER" --password="$DOCKER_PASSWORD" --email="dex@example.com" $DOCKER_REGISTRY
fi
docker push $repo:$version
docker push $repo:latest
}
clean() {
docker rmi $repo:$version
docker rmi $repo:latest
}
if [[ "clean" == "$1" ]]; then
clean
else
build
if [[ "push" == "$1" ]]; then
push
fi
fi

199
client/client.go Normal file
View File

@ -0,0 +1,199 @@
package client
import (
"encoding/base64"
"encoding/json"
"errors"
"io"
"net/url"
"reflect"
"strings"
"golang.org/x/crypto/bcrypt"
"github.com/coreos/dex/repo"
"github.com/coreos/go-oidc/oidc"
)
var (
ErrorInvalidClientID = errors.New("not a valid client ID")
ErrorInvalidClientSecret = errors.New("not a valid client Secret")
ErrorDuplicateClientID = errors.New("client ID already exists")
ErrorInvalidRedirectURL = errors.New("not a valid redirect url for the given client")
ErrorCantChooseRedirectURL = errors.New("must provide a redirect url; client has many")
ErrorNoValidRedirectURLs = errors.New("no valid redirect URLs for this client.")
ErrorPublicClientRedirectURIs = errors.New("public clients cannot have redirect URIs")
ErrorPublicClientMissingName = errors.New("public clients must have a name")
ErrorMissingRedirectURI = errors.New("no client redirect url given")
ErrorNotFound = errors.New("no data found")
)
type ValidationError struct {
Err error
}
func (v ValidationError) Error() string {
return v.Err.Error()
}
const (
bcryptHashCost = 10
OOBRedirectURI = "urn:ietf:wg:oauth:2.0:oob"
)
func HashSecret(creds oidc.ClientCredentials) ([]byte, error) {
secretBytes, err := base64.URLEncoding.DecodeString(creds.Secret)
if err != nil {
return nil, ErrorInvalidClientSecret
}
hashed, err := bcrypt.GenerateFromPassword([]byte(
secretBytes),
bcryptHashCost)
if err != nil {
return nil, err
}
return hashed, nil
}
type Client struct {
Credentials oidc.ClientCredentials
Metadata oidc.ClientMetadata
Admin bool
Public bool
}
func (c Client) ValidRedirectURL(u *url.URL) (url.URL, error) {
if c.Public {
if u == nil {
return url.URL{}, ErrorInvalidRedirectURL
}
if u.String() == OOBRedirectURI {
return *u, nil
}
if u.Scheme != "http" {
return url.URL{}, ErrorInvalidRedirectURL
}
hostPort := strings.Split(u.Host, ":")
if len(hostPort) != 2 {
return url.URL{}, ErrorInvalidRedirectURL
}
if hostPort[0] != "localhost" || u.Path != "" || u.RawPath != "" || u.RawQuery != "" || u.Fragment != "" {
return url.URL{}, ErrorInvalidRedirectURL
}
return *u, nil
}
return ValidRedirectURL(u, c.Metadata.RedirectURIs)
}
type ClientRepo interface {
Get(tx repo.Transaction, clientID string) (Client, error)
// GetSecret returns the (base64 encoded) hashed client secret
GetSecret(tx repo.Transaction, clientID string) ([]byte, error)
// All returns all registered Clients
All(tx repo.Transaction) ([]Client, error)
// New registers a Client with the repo.
// An unused ID must be provided. A corresponding secret will be returned
// in a ClientCredentials struct along with the provided ID.
New(tx repo.Transaction, client Client) (*oidc.ClientCredentials, error)
Update(tx repo.Transaction, client Client) error
// GetTrustedPeers returns the list of clients authorized to mint ID token for the given client.
GetTrustedPeers(tx repo.Transaction, clientID string) ([]string, error)
// SetTrustedPeers sets the list of clients authorized to mint ID token for the given client.
SetTrustedPeers(tx repo.Transaction, clientID string, clientIDs []string) error
}
// ValidRedirectURL returns the passed in URL if it is present in the redirectURLs list, and returns an error otherwise.
// If nil is passed in as the rURL and there is only one URL in redirectURLs,
// that URL will be returned. If nil is passed but theres >1 URL in the slice,
// then an error is returned.
func ValidRedirectURL(rURL *url.URL, redirectURLs []url.URL) (url.URL, error) {
if len(redirectURLs) == 0 {
return url.URL{}, ErrorNoValidRedirectURLs
}
if rURL == nil {
if len(redirectURLs) > 1 {
return url.URL{}, ErrorCantChooseRedirectURL
}
return redirectURLs[0], nil
}
for _, ru := range redirectURLs {
if reflect.DeepEqual(ru, *rURL) {
return ru, nil
}
}
return url.URL{}, ErrorInvalidRedirectURL
}
// LoadableClient contains sufficient information for creating a Client and its related entities.
type LoadableClient struct {
Client Client
TrustedPeers []string
}
func ClientsFromReader(r io.Reader) ([]LoadableClient, error) {
var c []struct {
ID string `json:"id"`
Secret string `json:"secret"`
RedirectURLs []string `json:"redirectURLs"`
Admin bool `json:"admin"`
Public bool `json:"public"`
TrustedPeers []string `json:"trustedPeers"`
}
if err := json.NewDecoder(r).Decode(&c); err != nil {
return nil, err
}
clients := make([]LoadableClient, len(c))
for i, client := range c {
if client.ID == "" {
return nil, errors.New("clients must have an ID")
}
if len(client.Secret) == 0 {
return nil, errors.New("clients must have a Secret")
}
redirectURIs := make([]url.URL, len(client.RedirectURLs))
for j, u := range client.RedirectURLs {
uri, err := url.Parse(u)
if err != nil {
return nil, err
}
redirectURIs[j] = *uri
}
clients[i] = LoadableClient{
Client: Client{
Credentials: oidc.ClientCredentials{
ID: client.ID,
Secret: client.Secret,
},
Metadata: oidc.ClientMetadata{
RedirectURIs: redirectURIs,
},
Admin: client.Admin,
Public: client.Public,
},
TrustedPeers: client.TrustedPeers,
}
}
return clients, nil
}

308
client/client_test.go Normal file
View File

@ -0,0 +1,308 @@
package client
import (
"encoding/base64"
"net/url"
"strings"
"testing"
"github.com/coreos/go-oidc/oidc"
"github.com/kylelemons/godebug/pretty"
)
var (
goodSecret1 = base64.URLEncoding.EncodeToString([]byte("my_secret"))
goodSecret2 = base64.URLEncoding.EncodeToString([]byte("my_other_secret"))
goodSecret3 = base64.URLEncoding.EncodeToString([]byte("yet_another_secret"))
goodClient1 = `{
"id": "my_id",
"secret": "` + goodSecret1 + `",
"redirectURLs": ["https://client.example.com"],
"admin": true
}`
goodClient2 = `{
"id": "my_other_id",
"secret": "` + goodSecret2 + `",
"redirectURLs": ["https://client2.example.com","https://client2_a.example.com"]
}`
goodClient3 = `{
"id": "yet_another_id",
"secret": "` + goodSecret3 + `",
"redirectURLs": ["https://client3.example.com","https://client3_a.example.com"],
"trustedPeers":["goodClient1", "goodClient2"]
}`
publicClient = `{
"id": "public_client",
"secret": "` + goodSecret3 + `",
"redirectURLs": ["http://localhost:8080","urn:ietf:wg:oauth:2.0:oob"],
"public": true
}`
badURLClient = `{
"id": "my_id",
"secret": "` + goodSecret1 + `",
"redirectURLs": ["hdtp:/\(bad)(u)(r)(l)"]
}`
badSecretClient = `{
"id": "my_id",
"secret": "` + "" + `",
"redirectURLs": ["https://client.example.com"]
}`
noSecretClient = `{
"id": "my_id",
"redirectURLs": ["https://client.example.com"]
}`
noIDClient = `{
"secret": "` + goodSecret1 + `",
"redirectURLs": ["https://client.example.com"]
}`
)
func TestClientsFromReader(t *testing.T) {
tests := []struct {
json string
want []LoadableClient
wantErr bool
}{
{
json: "[]",
want: []LoadableClient{},
},
{
json: "[" + goodClient1 + "]",
want: []LoadableClient{
{
Client: Client{
Credentials: oidc.ClientCredentials{
ID: "my_id",
Secret: goodSecret1,
},
Metadata: oidc.ClientMetadata{
RedirectURIs: []url.URL{
mustParseURL(t, "https://client.example.com"),
},
},
Admin: true,
},
},
},
},
{
json: "[" + strings.Join([]string{goodClient1, goodClient2}, ",") + "]",
want: []LoadableClient{
{
Client: Client{
Credentials: oidc.ClientCredentials{
ID: "my_id",
Secret: goodSecret1,
},
Metadata: oidc.ClientMetadata{
RedirectURIs: []url.URL{
mustParseURL(t, "https://client.example.com"),
},
},
Admin: true,
},
},
{
Client: Client{
Credentials: oidc.ClientCredentials{
ID: "my_other_id",
Secret: goodSecret2,
},
Metadata: oidc.ClientMetadata{
RedirectURIs: []url.URL{
mustParseURL(t, "https://client2.example.com"),
mustParseURL(t, "https://client2_a.example.com"),
},
},
},
},
},
},
{
json: "[" + goodClient3 + "]",
want: []LoadableClient{
{
Client: Client{
Credentials: oidc.ClientCredentials{
ID: "yet_another_id",
Secret: goodSecret3,
},
Metadata: oidc.ClientMetadata{
RedirectURIs: []url.URL{
mustParseURL(t, "https://client3.example.com"),
mustParseURL(t, "https://client3_a.example.com"),
},
},
},
TrustedPeers: []string{"goodClient1", "goodClient2"},
},
},
},
{
json: "[" + publicClient + "]",
want: []LoadableClient{
{
Client: Client{
Credentials: oidc.ClientCredentials{
ID: "public_client",
Secret: goodSecret3,
},
Metadata: oidc.ClientMetadata{
RedirectURIs: []url.URL{
mustParseURL(t, "http://localhost:8080"),
mustParseURL(t, "urn:ietf:wg:oauth:2.0:oob"),
},
},
Public: true,
},
},
},
},
{
json: "[" + badURLClient + "]",
wantErr: true,
},
{
json: "[" + badSecretClient + "]",
wantErr: true,
},
{
json: "[" + noSecretClient + "]",
wantErr: true,
},
{
json: "[" + noIDClient + "]",
wantErr: true,
},
}
for i, tt := range tests {
r := strings.NewReader(tt.json)
cs, err := ClientsFromReader(r)
if tt.wantErr {
if err == nil {
t.Errorf("case %d: want non-nil err", i)
t.Logf(pretty.Sprint(cs))
}
continue
}
if err != nil {
t.Errorf("case %d: got unexpected error parsing clients: %v", i, err)
t.Logf(tt.json)
}
if diff := pretty.Compare(tt.want, cs); diff != "" {
t.Errorf("case %d: Compare(want, got): %v", i, diff)
}
}
}
func TestClientValidRedirectURL(t *testing.T) {
makeClient := func(public bool, urls []string) Client {
cli := Client{
Metadata: oidc.ClientMetadata{
RedirectURIs: make([]url.URL, len(urls)),
},
Public: public,
}
for i, s := range urls {
cli.Metadata.RedirectURIs[i] = mustParseURL(t, s)
}
return cli
}
tests := []struct {
u string
cli Client
wantU string
wantErr bool
}{
{
u: "http://auth.example.com",
cli: makeClient(false, []string{"http://auth.example.com"}),
wantU: "http://auth.example.com",
},
{
u: "http://auth2.example.com",
cli: makeClient(false, []string{"http://auth.example.com", "http://auth2.example.com"}),
wantU: "http://auth2.example.com",
},
{
u: "",
cli: makeClient(false, []string{"http://auth.example.com"}),
wantU: "http://auth.example.com",
},
{
u: "",
cli: makeClient(false, []string{"http://auth.example.com", "http://auth2.example.com"}),
wantErr: true,
},
{
u: "http://localhost:8080",
cli: makeClient(true, []string{}),
wantU: "http://localhost:8080",
},
{
u: OOBRedirectURI,
cli: makeClient(true, []string{}),
wantU: OOBRedirectURI,
},
{
u: "",
cli: makeClient(true, []string{}),
wantErr: true,
},
{
u: "http://localhost:8080/hey_there",
cli: makeClient(true, []string{}),
wantErr: true,
},
{
u: "http://auth.google.com:8080",
cli: makeClient(true, []string{}),
wantErr: true,
},
}
for i, tt := range tests {
var testURL *url.URL
if tt.u == "" {
testURL = nil
} else {
u := mustParseURL(t, tt.u)
testURL = &u
}
u, err := tt.cli.ValidRedirectURL(testURL)
if tt.wantErr {
if err == nil {
t.Errorf("case %d: want non-nil error", i)
}
continue
}
if err != nil {
t.Errorf("case %d: unexpected error: %v", i, err)
}
if diff := pretty.Compare(mustParseURL(t, tt.wantU), u); diff != "" {
t.Fatalf("case %d: Compare(wantU, u): %v", i, diff)
}
}
}
func mustParseURL(t *testing.T, s string) url.URL {
u, err := url.Parse(s)
if err != nil {
t.Fatalf("Cannot parse %v as url: %v", s, err)
}
return *u
}

262
client/manager/manager.go Normal file
View File

@ -0,0 +1,262 @@
package manager
import (
"encoding/base64"
"net/url"
"errors"
"github.com/coreos/dex/client"
pcrypto "github.com/coreos/dex/pkg/crypto"
"github.com/coreos/dex/pkg/log"
"github.com/coreos/dex/repo"
"github.com/coreos/go-oidc/oidc"
"golang.org/x/crypto/bcrypt"
)
const (
// Blowfish, the algorithm underlying bcrypt, has a maximum
// password length of 72. We explicitly track and check this
// since the bcrypt library will silently ignore portions of
// a password past the first 72 characters.
maxSecretLength = 72
)
var (
localHostRedirectURL = mustParseURL("http://localhost:0")
)
type ClientOptions struct {
TrustedPeers []string
}
type SecretGenerator func() ([]byte, error)
func DefaultSecretGenerator() ([]byte, error) {
return pcrypto.RandBytes(maxSecretLength)
}
func CompareHashAndPassword(hashedPassword, password []byte) error {
if len(password) > maxSecretLength {
return errors.New("password length greater than max secret length")
}
return bcrypt.CompareHashAndPassword(hashedPassword, password)
}
// ClientManager performs client-related "business-logic" functions on client and related objects.
// This is in contrast to the Repos which perform little more than CRUD operations.
type ClientManager struct {
clientRepo client.ClientRepo
begin repo.TransactionFactory
secretGenerator SecretGenerator
clientIDGenerator func(string) (string, error)
}
type ManagerOptions struct {
SecretGenerator func() ([]byte, error)
ClientIDGenerator func(string) (string, error)
}
func NewClientManager(clientRepo client.ClientRepo, txnFactory repo.TransactionFactory, options ManagerOptions) *ClientManager {
if options.SecretGenerator == nil {
options.SecretGenerator = DefaultSecretGenerator
}
if options.ClientIDGenerator == nil {
options.ClientIDGenerator = oidc.GenClientID
}
return &ClientManager{
clientRepo: clientRepo,
begin: txnFactory,
secretGenerator: options.SecretGenerator,
clientIDGenerator: options.ClientIDGenerator,
}
}
// New creates and persists a new client with the given options, returning the generated credentials.
// Any Credenials provided with the client are ignored and overwritten by the generated ID and Secret.
// "Normal" (i.e. non-Public) clients must have at least one valid RedirectURI in their Metadata.
// Public clients must not have any RedirectURIs and must have a client name.
func (m *ClientManager) New(cli client.Client, options *ClientOptions) (*oidc.ClientCredentials, error) {
tx, err := m.begin()
if err != nil {
return nil, err
}
defer tx.Rollback()
if err := validateClient(cli); err != nil {
return nil, err
}
err = m.addClientCredentials(&cli)
if err != nil {
return nil, err
}
creds := cli.Credentials
// Save Client
_, err = m.clientRepo.New(tx, cli)
if err != nil {
return nil, err
}
if options != nil && len(options.TrustedPeers) > 0 {
err = m.clientRepo.SetTrustedPeers(tx, creds.ID, options.TrustedPeers)
if err != nil {
return nil, err
}
}
err = tx.Commit()
if err != nil {
return nil, err
}
// Returns creds with unhashed secret
return &creds, nil
}
func (m *ClientManager) Get(id string) (client.Client, error) {
return m.clientRepo.Get(nil, id)
}
func (m *ClientManager) All() ([]client.Client, error) {
return m.clientRepo.All(nil)
}
func (m *ClientManager) Metadata(clientID string) (*oidc.ClientMetadata, error) {
c, err := m.clientRepo.Get(nil, clientID)
if err != nil {
return nil, err
}
return &c.Metadata, nil
}
func (m *ClientManager) IsDexAdmin(clientID string) (bool, error) {
c, err := m.clientRepo.Get(nil, clientID)
if err != nil {
return false, err
}
return c.Admin, nil
}
func (m *ClientManager) SetDexAdmin(clientID string, isAdmin bool) error {
tx, err := m.begin()
if err != nil {
return err
}
defer tx.Rollback()
c, err := m.clientRepo.Get(tx, clientID)
if err != nil {
return err
}
c.Admin = isAdmin
err = m.clientRepo.Update(tx, c)
if err != nil {
return err
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func (m *ClientManager) Authenticate(creds oidc.ClientCredentials) (bool, error) {
clientSecret, err := m.clientRepo.GetSecret(nil, creds.ID)
if err != nil {
log.Errorf("error getting secret for client ID: %v: err: %v", creds.ID, err)
return false, nil
}
if clientSecret == nil {
log.Errorf("no secret found for client ID: %v", creds.ID)
return false, nil
}
dec, err := base64.URLEncoding.DecodeString(creds.Secret)
if err != nil {
log.Errorf("error Decoding client creds: %v", err)
return false, nil
}
ok := CompareHashAndPassword(clientSecret, dec) == nil
return ok, nil
}
func (m *ClientManager) addClientCredentials(cli *client.Client) error {
var seed string
if cli.Public {
seed = cli.Metadata.ClientName
} else {
seed = cli.Metadata.RedirectURIs[0].Host
}
var err error
var clientID string
if cli.Credentials.ID != "" {
clientID = cli.Credentials.ID
} else {
// Generate Client ID
clientID, err = m.clientIDGenerator(seed)
if err != nil {
return err
}
}
var clientSecret string
if cli.Credentials.Secret != "" {
clientSecret = cli.Credentials.Secret
} else {
// Generate Secret
secret, err := m.secretGenerator()
if err != nil {
return err
}
clientSecret = base64.URLEncoding.EncodeToString(secret)
}
cli.Credentials = oidc.ClientCredentials{
ID: clientID,
Secret: clientSecret,
}
return nil
}
func validateClient(cli client.Client) error {
// NOTE: please be careful changing the errors returned here; they are used
// downstream (eg. in the admin API) to determine the http errors returned.
if cli.Public {
if len(cli.Metadata.RedirectURIs) > 0 {
return client.ErrorPublicClientRedirectURIs
}
if cli.Metadata.ClientName == "" {
return client.ErrorPublicClientMissingName
}
cli.Metadata.RedirectURIs = []url.URL{
localHostRedirectURL,
}
} else {
if len(cli.Metadata.RedirectURIs) < 1 {
return client.ErrorMissingRedirectURI
}
}
err := cli.Metadata.Valid()
if err != nil {
return client.ValidationError{Err: err}
}
return nil
}
func mustParseURL(s string) url.URL {
u, err := url.Parse(s)
if err != nil {
panic(err)
}
return *u
}

View File

@ -0,0 +1,220 @@
package manager
import (
"encoding/base64"
"fmt"
"net/url"
"testing"
"github.com/coreos/dex/client"
"github.com/coreos/dex/db"
"github.com/coreos/go-oidc/oidc"
)
type testFixtures struct {
clientRepo client.ClientRepo
mgr *ClientManager
}
var (
goodSecret = base64.URLEncoding.EncodeToString([]byte("secret"))
)
func makeTestFixtures() *testFixtures {
f := &testFixtures{}
dbMap := db.NewMemDB()
clients := []client.LoadableClient{
{
Client: client.Client{
Credentials: oidc.ClientCredentials{
ID: "client.example.com",
Secret: goodSecret,
},
Metadata: oidc.ClientMetadata{
RedirectURIs: []url.URL{
{Scheme: "http", Host: "client.example.com", Path: "/"},
},
},
Admin: true,
},
},
}
clientIDGenerator := func(hostport string) (string, error) {
return hostport, nil
}
secGen := func() ([]byte, error) {
return []byte("secret"), nil
}
var err error
f.clientRepo, err = db.NewClientRepoFromClients(dbMap, clients)
if err != nil {
panic("Failed to create client manager: " + err.Error())
}
clientManager := NewClientManager(f.clientRepo, db.TransactionFactory(dbMap), ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen})
f.mgr = clientManager
return f
}
func TestMetadata(t *testing.T) {
tests := []struct {
clientID string
uri string
wantErr bool
}{
{
clientID: "client.example.com",
uri: "http://client.example.com/",
wantErr: false,
},
}
for i, tt := range tests {
f := makeTestFixtures()
md, err := f.mgr.Metadata(tt.clientID)
if err != nil {
t.Errorf("case %d: unexpected err: %v", i, err)
continue
}
if md.RedirectURIs[0].String() != tt.uri {
t.Errorf("case %d: manager.Metadata.RedirectURIs: want=%q got=%q", i, tt.uri, md.RedirectURIs[0].String())
continue
}
}
}
func TestIsDexAdmin(t *testing.T) {
tests := []struct {
clientID string
isAdmin bool
wantErr bool
}{
{
clientID: "client.example.com",
isAdmin: true,
wantErr: false,
},
}
for i, tt := range tests {
f := makeTestFixtures()
admin, err := f.mgr.IsDexAdmin(tt.clientID)
if err != nil {
t.Errorf("case %d: unexpected err: %v", i, err)
continue
}
if admin != tt.isAdmin {
t.Errorf("case %d: manager.Admin want=%t got=%t", i, tt.isAdmin, admin)
continue
}
}
}
func TestSetDexAdmin(t *testing.T) {
f := makeTestFixtures()
err := f.mgr.SetDexAdmin("client.example.com", false)
if err != nil {
t.Errorf("unexpected err: %v", err)
}
admin, _ := f.mgr.IsDexAdmin("client.example.com")
if admin {
t.Errorf("expected admin to be false")
}
}
func TestAuthenticate(t *testing.T) {
f := makeTestFixtures()
cm := oidc.ClientMetadata{
RedirectURIs: []url.URL{
url.URL{Scheme: "http", Host: "example.com", Path: "/cb"},
},
}
cli := client.Client{
Metadata: cm,
}
cc, err := f.mgr.New(cli, nil)
if err != nil {
t.Fatalf(err.Error())
}
ok, err := f.mgr.Authenticate(*cc)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
} else if !ok {
t.Fatalf("Authentication failed for good creds")
}
creds := []oidc.ClientCredentials{
//completely made up
oidc.ClientCredentials{ID: "foo", Secret: "bar"},
// good client ID, bad secret
oidc.ClientCredentials{ID: cc.ID, Secret: "bar"},
// bad client ID, good secret
oidc.ClientCredentials{ID: "foo", Secret: cc.Secret},
// good client ID, secret with some fluff on the end
oidc.ClientCredentials{ID: cc.ID, Secret: fmt.Sprintf("%sfluff", cc.Secret)},
}
for i, c := range creds {
ok, err := f.mgr.Authenticate(c)
if err != nil {
t.Errorf("case %d: unexpected error: %v", i, err)
} else if ok {
t.Errorf("case %d: authentication succeeded for bad creds", i)
}
}
}
func TestValidateClient(t *testing.T) {
tests := []struct {
cli client.Client
wantErr error
}{
{
cli: client.Client{
Metadata: oidc.ClientMetadata{
RedirectURIs: []url.URL{mustParseURL("http://auth.google.com")},
},
},
},
{
cli: client.Client{},
wantErr: client.ErrorMissingRedirectURI,
},
{
cli: client.Client{
Metadata: oidc.ClientMetadata{
ClientName: "frank",
},
Public: true,
},
},
{
cli: client.Client{
Metadata: oidc.ClientMetadata{
RedirectURIs: []url.URL{mustParseURL("http://auth.google.com")},
ClientName: "frank",
},
Public: true,
},
wantErr: client.ErrorPublicClientRedirectURIs,
},
{
cli: client.Client{
Public: true,
},
wantErr: client.ErrorPublicClientMissingName,
},
}
for i, tt := range tests {
err := validateClient(tt.cli)
if err != tt.wantErr {
t.Errorf("case %d: want=%v, got=%v", i, tt.wantErr, err)
}
}
}

170
cmd/dex-overlord/main.go Normal file
View File

@ -0,0 +1,170 @@
package main
import (
"expvar"
"flag"
"fmt"
"net/http"
"net/url"
"os"
"runtime"
"strings"
"time"
"github.com/coreos/go-oidc/key"
"github.com/go-gorp/gorp"
"github.com/coreos/dex/admin"
clientmanager "github.com/coreos/dex/client/manager"
"github.com/coreos/dex/db"
pflag "github.com/coreos/dex/pkg/flag"
"github.com/coreos/dex/pkg/log"
ptime "github.com/coreos/dex/pkg/time"
"github.com/coreos/dex/server"
"github.com/coreos/dex/user/manager"
)
var version = "DEV"
func init() {
expvar.NewString("dex.version").Set(version)
}
func main() {
fs := flag.NewFlagSet("dex-overlord", flag.ExitOnError)
keySecrets := pflag.NewBase64List(32)
fs.Var(keySecrets, "key-secrets", "A comma-separated list of base64 encoded 32 byte strings used as symmetric keys used to encrypt/decrypt signing key data in DB. The first key is considered the active key and used for encryption, while the others are used to decrypt.")
useOldFormat := fs.Bool("use-deprecated-secret-format", false, "In prior releases, the database used AES-CBC to encrypt keys. New deployments should use the default AES-GCM encryption.")
dbURL := fs.String("db-url", "", "DSN-formatted database connection string")
dbMigrate := fs.Bool("db-migrate", true, "perform database migrations when starting up overlord. This includes the initial DB objects creation.")
keyPeriod := fs.Duration("key-period", 24*time.Hour, "length of time for-which a given key will be valid")
gcInterval := fs.Duration("gc-interval", time.Hour, "length of time between garbage collection runs")
adminListen := fs.String("admin-listen", "http://127.0.0.1:5557", "scheme, host and port for listening for administrative operation requests ")
adminAPISecret := pflag.NewBase64(server.AdminAPISecretLength)
fs.Var(adminAPISecret, "admin-api-secret", fmt.Sprintf("A base64-encoded %d byte string which is used to protect the Admin API.", server.AdminAPISecretLength))
localConnectorID := fs.String("local-connector", "local", "ID of the local connector")
logDebug := fs.Bool("log-debug", false, "log debug-level information")
logTimestamps := fs.Bool("log-timestamps", false, "prefix log lines with timestamps")
printVersion := fs.Bool("version", false, "Print the version and exit")
if err := fs.Parse(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
if err := pflag.SetFlagsFromEnv(fs, "DEX_OVERLORD"); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
if *printVersion {
fmt.Printf("dex version %s\ngo version %s\n", strings.TrimPrefix(version, "v"), strings.TrimPrefix(runtime.Version(), "go"))
os.Exit(0)
}
if *logDebug {
log.EnableDebug()
}
if *logTimestamps {
log.EnableTimestamps()
}
adminURL, err := url.Parse(*adminListen)
if err != nil {
log.Fatalf("Unable to use --admin-listen flag: %v", err)
}
if len(keySecrets.BytesSlice()) == 0 {
log.Fatalf("Must specify at least one key secret")
}
dbCfg := db.Config{
DSN: *dbURL,
MaxIdleConnections: 1,
MaxOpenConnections: 1,
}
dbc, err := db.NewConnection(dbCfg)
if err != nil {
log.Fatalf(err.Error())
}
if _, ok := dbc.Dialect.(gorp.PostgresDialect); !ok {
log.Fatal("only postgres backend supported for multi server configurations")
}
if *dbMigrate {
var sleep time.Duration
for {
var err error
var migrations int
if migrations, err = db.MigrateToLatest(dbc); err == nil {
log.Infof("Performed %d db migrations", migrations)
break
}
sleep = ptime.ExpBackoff(sleep, time.Minute)
log.Errorf("Unable to migrate database, retrying in %v: %v", sleep, err)
time.Sleep(sleep)
}
}
userRepo := db.NewUserRepo(dbc)
pwiRepo := db.NewPasswordInfoRepo(dbc)
connCfgRepo := db.NewConnectorConfigRepo(dbc)
clientRepo := db.NewClientRepo(dbc)
userManager := manager.NewUserManager(userRepo,
pwiRepo, connCfgRepo, db.TransactionFactory(dbc), manager.ManagerOptions{})
clientManager := clientmanager.NewClientManager(clientRepo, db.TransactionFactory(dbc), clientmanager.ManagerOptions{})
connectorConfigRepo := db.NewConnectorConfigRepo(dbc)
adminAPI := admin.NewAdminAPI(userRepo, pwiRepo, clientRepo, connectorConfigRepo, userManager, clientManager, *localConnectorID)
kRepo, err := db.NewPrivateKeySetRepo(dbc, *useOldFormat, keySecrets.BytesSlice()...)
if err != nil {
log.Fatalf(err.Error())
}
var sleep time.Duration
for {
var done bool
_, err := kRepo.Get()
switch err {
case nil:
done = true
case key.ErrorNoKeys:
done = true
case db.ErrorCannotDecryptKeys:
log.Fatalf("Cannot decrypt keys using any of the given key secrets. The key secrets must be changed to include one that can decrypt the existing keys, or the existing keys must be deleted.")
}
if done {
break
}
sleep = ptime.ExpBackoff(sleep, time.Minute)
log.Errorf("Unable to get keys from repository, retrying in %v: %v", sleep, err)
time.Sleep(sleep)
}
krot := key.NewPrivateKeyRotator(kRepo, *keyPeriod)
s := server.NewAdminServer(adminAPI, krot, adminAPISecret.String())
h := s.HTTPHandler()
httpsrv := &http.Server{
Addr: adminURL.Host,
Handler: h,
}
gc := db.NewGarbageCollector(dbc, *gcInterval)
log.Infof("Binding to %s...", httpsrv.Addr)
go func() {
log.Fatal(httpsrv.ListenAndServe())
}()
gc.Run()
<-krot.Run()
}

233
cmd/dex-worker/main.go Normal file
View File

@ -0,0 +1,233 @@
package main
import (
"expvar"
"flag"
"fmt"
"net/http"
"net/url"
"os"
"runtime"
"strings"
"time"
"github.com/coreos/pkg/flagutil"
"github.com/gorilla/handlers"
"github.com/coreos/dex/connector"
"github.com/coreos/dex/db"
pflag "github.com/coreos/dex/pkg/flag"
"github.com/coreos/dex/pkg/log"
ptime "github.com/coreos/dex/pkg/time"
"github.com/coreos/dex/server"
)
var version = "DEV"
func init() {
versionVar := expvar.NewString("dex.version")
versionVar.Set(version)
}
func main() {
fs := flag.NewFlagSet("dex-worker", flag.ExitOnError)
listen := fs.String("listen", "http://127.0.0.1:5556", "the address that the server will listen on")
issuer := fs.String("issuer", "http://127.0.0.1:5556/dex", "the issuer's location")
certFile := fs.String("tls-cert-file", "", "the server's certificate file for TLS connection")
keyFile := fs.String("tls-key-file", "", "the server's private key file for TLS connection")
templates := fs.String("html-assets", "./static/html", "directory of html template files")
emailTemplateDirs := flagutil.StringSliceFlag{"./static/email"}
fs.Var(&emailTemplateDirs, "email-templates", "comma separated list of directories of email template files")
emailFrom := fs.String("email-from", "", `DEPRICATED: use "from" field in email config.`)
emailConfig := fs.String("email-cfg", "./static/fixtures/emailer.json", "configures emailer.")
enableRegistration := fs.Bool("enable-registration", false, "Allows users to self-register. This flag cannot be used in combination with --enable-automatic-registration.")
registerOnFirstLogin := fs.Bool("enable-automatic-registration", false, "When a user logs in through a federated identity service, automatically register them if they don't have an account. This flag cannot be used in combination with --enable-registration.")
enableClientRegistration := fs.Bool("enable-client-registration", false, "Allow dynamic registration of clients")
// Client credentials administration
apiUseClientCredentials := fs.Bool("api-use-client-credentials", false, "Forces API to authenticate using client credentials instead of ID token. Clients must be 'admin clients' to use the API.")
noDB := fs.Bool("no-db", false, "manage entities in-process w/o any encryption, used only for single-node testing")
// UI-related:
issuerName := fs.String("issuer-name", "dex", "The name of this dex installation; will appear on most pages.")
issuerLogoURL := fs.String("issuer-logo-url", "https://coreos.com/assets/images/brand/coreos-wordmark-135x40px.png", "URL of an image representing the issuer")
// ignored if --no-db is set
dbURL := fs.String("db-url", "", "DSN-formatted database connection string")
keySecrets := pflag.NewBase64List(32)
fs.Var(keySecrets, "key-secrets", "A comma-separated list of base64 encoded 32 byte strings used as symmetric keys used to encrypt/decrypt signing key data in DB. The first key is considered the active key and used for encryption, while the others are used to decrypt.")
useOldFormat := fs.Bool("use-deprecated-secret-format", false, "In prior releases, the database used AES-CBC to encrypt keys. New deployments should use the default AES-GCM encryption.")
dbMaxIdleConns := fs.Int("db-max-idle-conns", 0, "maximum number of connections in the idle connection pool")
dbMaxOpenConns := fs.Int("db-max-open-conns", 0, "maximum number of open connections to the database")
printVersion := fs.Bool("version", false, "Print the version and exit")
// These are configuration files for development convenience, only used if --no-db is set.
connectors := fs.String("connectors", "./static/fixtures/connectors.json.sample", "JSON file containg set of IDPC configs")
clients := fs.String("clients", "./static/fixtures/clients.json.sample", "json file containing set of clients")
users := fs.String("users", "./static/fixtures/users.json.sample", "json file containing set of users")
logDebug := fs.Bool("log-debug", false, "log debug-level information")
logTimestamps := fs.Bool("log-timestamps", false, "prefix log lines with timestamps")
if err := fs.Parse(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
if err := pflag.SetFlagsFromEnv(fs, "DEX_WORKER"); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
if *printVersion {
fmt.Printf("dex version %s\ngo version %s\n", strings.TrimPrefix(version, "v"), strings.TrimPrefix(runtime.Version(), "go"))
os.Exit(0)
}
if (*enableRegistration) && (*registerOnFirstLogin) {
fmt.Fprintln(os.Stderr, "The flags --enable-registration and --enable-automatic-login cannot both be true.")
os.Exit(1)
}
if *logDebug {
log.EnableDebug()
log.Infof("Debug logging enabled.")
log.Debugf("Debug logging enabled.")
}
if *logTimestamps {
log.EnableTimestamps()
}
// Validate listen address.
lu, err := url.Parse(*listen)
if err != nil {
log.Fatalf("Invalid listen address %q: %v", *listen, err)
}
switch lu.Scheme {
case "http":
case "https":
if *certFile == "" || *keyFile == "" {
log.Fatalf("Must provide certificate file and private key file")
}
default:
log.Fatalf("Only 'http' and 'https' schemes are supported")
}
// Validate issuer address.
iu, err := url.Parse(*issuer)
if err != nil {
log.Fatalf("Invalid issuer URL %q: %v", *issuer, err)
}
if iu.Scheme != "http" && iu.Scheme != "https" {
log.Fatalf("Only 'http' and 'https' schemes are supported")
}
if *emailFrom != "" {
log.Errorf(`--email-from flag is depricated. Use "from" field in email config.`)
}
scfg := server.ServerConfig{
IssuerURL: *issuer,
TemplateDir: *templates,
EmailTemplateDirs: emailTemplateDirs,
EmailFromAddress: *emailFrom,
EmailerConfigFile: *emailConfig,
IssuerName: *issuerName,
IssuerLogoURL: *issuerLogoURL,
EnableRegistration: *enableRegistration,
EnableClientRegistration: *enableClientRegistration,
EnableClientCredentialAccess: *apiUseClientCredentials,
RegisterOnFirstLogin: *registerOnFirstLogin,
}
if *noDB {
log.Warning("Running in-process without external database or key rotation")
scfg.StateConfig = &server.SingleServerConfig{
ClientsFile: *clients,
ConnectorsFile: *connectors,
UsersFile: *users,
}
} else {
if len(keySecrets.BytesSlice()) == 0 {
log.Fatalf("Must specify at least one key secret")
}
if *dbMaxIdleConns == 0 {
log.Warning("Running with no limit on: database idle connections")
}
if *dbMaxOpenConns == 0 {
log.Warning("Running with no limit on: database open connections")
}
dbCfg := db.Config{
DSN: *dbURL,
MaxIdleConnections: *dbMaxIdleConns,
MaxOpenConnections: *dbMaxOpenConns,
}
scfg.StateConfig = &server.MultiServerConfig{
KeySecrets: keySecrets.BytesSlice(),
DatabaseConfig: dbCfg,
UseOldFormat: *useOldFormat,
}
}
srv, err := scfg.Server()
if err != nil {
log.Fatalf("Unable to build Server: %v", err)
}
var cfgs []connector.ConnectorConfig
var sleep time.Duration
for {
var err error
cfgs, err = srv.ConnectorConfigRepo.All()
if len(cfgs) > 0 && err == nil {
break
}
sleep = ptime.ExpBackoff(sleep, 8*time.Second)
if err != nil {
log.Errorf("Unable to load connectors, retrying in %v: %v", sleep, err)
} else {
log.Errorf("No connectors, will wait. Retrying in %v.", sleep)
}
time.Sleep(sleep)
}
for _, cfg := range cfgs {
cfg := cfg
if err = srv.AddConnector(cfg); err != nil {
log.Fatalf("Failed registering connector '%s': %v", cfg.ConnectorID(), err)
}
}
h := srv.HTTPHandler()
h = handlers.LoggingHandler(log.InfoWriter(), h)
httpsrv := &http.Server{
Addr: lu.Host,
Handler: h,
}
log.Infof("Binding to %s...", httpsrv.Addr)
go func() {
if lu.Scheme == "http" {
log.Fatal(httpsrv.ListenAndServe())
} else {
log.Fatal(httpsrv.ListenAndServeTLS(*certFile, *keyFile))
}
}()
<-srv.Run()
}

View File

@ -1,352 +0,0 @@
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"golang.org/x/crypto/bcrypt"
"github.com/dexidp/dex/pkg/log"
"github.com/dexidp/dex/server"
"github.com/dexidp/dex/storage"
"github.com/dexidp/dex/storage/ent"
"github.com/dexidp/dex/storage/etcd"
"github.com/dexidp/dex/storage/kubernetes"
"github.com/dexidp/dex/storage/memory"
"github.com/dexidp/dex/storage/sql"
)
// Config is the config format for the main application.
type Config struct {
Issuer string `json:"issuer"`
Storage Storage `json:"storage"`
Web Web `json:"web"`
Telemetry Telemetry `json:"telemetry"`
OAuth2 OAuth2 `json:"oauth2"`
GRPC GRPC `json:"grpc"`
Expiry Expiry `json:"expiry"`
Logger Logger `json:"logger"`
Frontend server.WebConfig `json:"frontend"`
// StaticConnectors are user defined connectors specified in the ConfigMap
// Write operations, like updating a connector, will fail.
StaticConnectors []Connector `json:"connectors"`
// StaticClients cause the server to use this list of clients rather than
// querying the storage. Write operations, like creating a client, will fail.
StaticClients []storage.Client `json:"staticClients"`
// If enabled, the server will maintain a list of passwords which can be used
// to identify a user.
EnablePasswordDB bool `json:"enablePasswordDB"`
// StaticPasswords cause the server use this list of passwords rather than
// querying the storage. Cannot be specified without enabling a passwords
// database.
StaticPasswords []password `json:"staticPasswords"`
}
// Validate the configuration
func (c Config) Validate() error {
// Fast checks. Perform these first for a more responsive CLI.
checks := []struct {
bad bool
errMsg string
}{
{c.Issuer == "", "no issuer specified in config file"},
{!c.EnablePasswordDB && len(c.StaticPasswords) != 0, "cannot specify static passwords without enabling password db"},
{c.Storage.Config == nil, "no storage supplied in config file"},
{c.Web.HTTP == "" && c.Web.HTTPS == "", "must supply a HTTP/HTTPS address to listen on"},
{c.Web.HTTPS != "" && c.Web.TLSCert == "", "no cert specified for HTTPS"},
{c.Web.HTTPS != "" && c.Web.TLSKey == "", "no private key specified for HTTPS"},
{c.GRPC.TLSCert != "" && c.GRPC.Addr == "", "no address specified for gRPC"},
{c.GRPC.TLSKey != "" && c.GRPC.Addr == "", "no address specified for gRPC"},
{(c.GRPC.TLSCert == "") != (c.GRPC.TLSKey == ""), "must specific both a gRPC TLS cert and key"},
{c.GRPC.TLSCert == "" && c.GRPC.TLSClientCA != "", "cannot specify gRPC TLS client CA without a gRPC TLS cert"},
}
var checkErrors []string
for _, check := range checks {
if check.bad {
checkErrors = append(checkErrors, check.errMsg)
}
}
if len(checkErrors) != 0 {
return fmt.Errorf("invalid Config:\n\t-\t%s", strings.Join(checkErrors, "\n\t-\t"))
}
return nil
}
type password storage.Password
func (p *password) UnmarshalJSON(b []byte) error {
var data struct {
Email string `json:"email"`
Username string `json:"username"`
UserID string `json:"userID"`
Hash string `json:"hash"`
HashFromEnv string `json:"hashFromEnv"`
}
if err := json.Unmarshal(b, &data); err != nil {
return err
}
*p = password(storage.Password{
Email: data.Email,
Username: data.Username,
UserID: data.UserID,
})
if len(data.Hash) == 0 && len(data.HashFromEnv) > 0 {
data.Hash = os.Getenv(data.HashFromEnv)
}
if len(data.Hash) == 0 {
return fmt.Errorf("no password hash provided")
}
// If this value is a valid bcrypt, use it.
_, bcryptErr := bcrypt.Cost([]byte(data.Hash))
if bcryptErr == nil {
p.Hash = []byte(data.Hash)
return nil
}
// For backwards compatibility try to base64 decode this value.
hashBytes, err := base64.StdEncoding.DecodeString(data.Hash)
if err != nil {
return fmt.Errorf("malformed bcrypt hash: %v", bcryptErr)
}
if _, err := bcrypt.Cost(hashBytes); err != nil {
return fmt.Errorf("malformed bcrypt hash: %v", err)
}
p.Hash = hashBytes
return nil
}
// OAuth2 describes enabled OAuth2 extensions.
type OAuth2 struct {
ResponseTypes []string `json:"responseTypes"`
// If specified, do not prompt the user to approve client authorization. The
// act of logging in implies authorization.
SkipApprovalScreen bool `json:"skipApprovalScreen"`
// If specified, show the connector selection screen even if there's only one
AlwaysShowLoginScreen bool `json:"alwaysShowLoginScreen"`
// This is the connector that can be used for password grant
PasswordConnector string `json:"passwordConnector"`
}
// Web is the config format for the HTTP server.
type Web struct {
HTTP string `json:"http"`
HTTPS string `json:"https"`
TLSCert string `json:"tlsCert"`
TLSKey string `json:"tlsKey"`
AllowedOrigins []string `json:"allowedOrigins"`
}
// Telemetry is the config format for telemetry including the HTTP server config.
type Telemetry struct {
HTTP string `json:"http"`
// EnableProfiling makes profiling endpoints available via web interface host:port/debug/pprof/
EnableProfiling bool `json:"enableProfiling"`
}
// GRPC is the config for the gRPC API.
type GRPC struct {
// The port to listen on.
Addr string `json:"addr"`
TLSCert string `json:"tlsCert"`
TLSKey string `json:"tlsKey"`
TLSClientCA string `json:"tlsClientCA"`
Reflection bool `json:"reflection"`
}
// Storage holds app's storage configuration.
type Storage struct {
Type string `json:"type"`
Config StorageConfig `json:"config"`
}
// StorageConfig is a configuration that can create a storage.
type StorageConfig interface {
Open(logger log.Logger) (storage.Storage, error)
}
var (
_ StorageConfig = (*etcd.Etcd)(nil)
_ StorageConfig = (*kubernetes.Config)(nil)
_ StorageConfig = (*memory.Config)(nil)
_ StorageConfig = (*sql.SQLite3)(nil)
_ StorageConfig = (*sql.Postgres)(nil)
_ StorageConfig = (*sql.MySQL)(nil)
_ StorageConfig = (*ent.SQLite3)(nil)
_ StorageConfig = (*ent.Postgres)(nil)
_ StorageConfig = (*ent.MySQL)(nil)
)
func getORMBasedSQLStorage(normal, entBased StorageConfig) func() StorageConfig {
return func() StorageConfig {
switch os.Getenv("DEX_ENT_ENABLED") {
case "true", "yes":
return entBased
default:
return normal
}
}
}
var storages = map[string]func() StorageConfig{
"etcd": func() StorageConfig { return new(etcd.Etcd) },
"kubernetes": func() StorageConfig { return new(kubernetes.Config) },
"memory": func() StorageConfig { return new(memory.Config) },
"sqlite3": getORMBasedSQLStorage(&sql.SQLite3{}, &ent.SQLite3{}),
"postgres": getORMBasedSQLStorage(&sql.Postgres{}, &ent.Postgres{}),
"mysql": getORMBasedSQLStorage(&sql.MySQL{}, &ent.MySQL{}),
}
// isExpandEnvEnabled returns if os.ExpandEnv should be used for each storage and connector config.
// Disabling this feature avoids surprises e.g. if the LDAP bind password contains a dollar character.
// Returns false if the env variable "DEX_EXPAND_ENV" is a falsy string, e.g. "false".
// Returns true if the env variable is unset or a truthy string, e.g. "true", or can't be parsed as bool.
func isExpandEnvEnabled() bool {
enabled, err := strconv.ParseBool(os.Getenv("DEX_EXPAND_ENV"))
if err != nil {
// Unset, empty string or can't be parsed as bool: Default = true.
return true
}
return enabled
}
// UnmarshalJSON allows Storage to implement the unmarshaler interface to
// dynamically determine the type of the storage config.
func (s *Storage) UnmarshalJSON(b []byte) error {
var store struct {
Type string `json:"type"`
Config json.RawMessage `json:"config"`
}
if err := json.Unmarshal(b, &store); err != nil {
return fmt.Errorf("parse storage: %v", err)
}
f, ok := storages[store.Type]
if !ok {
return fmt.Errorf("unknown storage type %q", store.Type)
}
storageConfig := f()
if len(store.Config) != 0 {
data := []byte(store.Config)
if isExpandEnvEnabled() {
// Caution, we're expanding in the raw JSON/YAML source. This may not be what the admin expects.
data = []byte(os.ExpandEnv(string(store.Config)))
}
if err := json.Unmarshal(data, storageConfig); err != nil {
return fmt.Errorf("parse storage config: %v", err)
}
}
*s = Storage{
Type: store.Type,
Config: storageConfig,
}
return nil
}
// Connector is a magical type that can unmarshal YAML dynamically. The
// Type field determines the connector type, which is then customized for Config.
type Connector struct {
Type string `json:"type"`
Name string `json:"name"`
ID string `json:"id"`
Config server.ConnectorConfig `json:"config"`
}
// UnmarshalJSON allows Connector to implement the unmarshaler interface to
// dynamically determine the type of the connector config.
func (c *Connector) UnmarshalJSON(b []byte) error {
var conn struct {
Type string `json:"type"`
Name string `json:"name"`
ID string `json:"id"`
Config json.RawMessage `json:"config"`
}
if err := json.Unmarshal(b, &conn); err != nil {
return fmt.Errorf("parse connector: %v", err)
}
f, ok := server.ConnectorsConfig[conn.Type]
if !ok {
return fmt.Errorf("unknown connector type %q", conn.Type)
}
connConfig := f()
if len(conn.Config) != 0 {
data := []byte(conn.Config)
if isExpandEnvEnabled() {
// Caution, we're expanding in the raw JSON/YAML source. This may not be what the admin expects.
data = []byte(os.ExpandEnv(string(conn.Config)))
}
if err := json.Unmarshal(data, connConfig); err != nil {
return fmt.Errorf("parse connector config: %v", err)
}
}
*c = Connector{
Type: conn.Type,
Name: conn.Name,
ID: conn.ID,
Config: connConfig,
}
return nil
}
// ToStorageConnector converts an object to storage connector type.
func ToStorageConnector(c Connector) (storage.Connector, error) {
data, err := json.Marshal(c.Config)
if err != nil {
return storage.Connector{}, fmt.Errorf("failed to marshal connector config: %v", err)
}
return storage.Connector{
ID: c.ID,
Type: c.Type,
Name: c.Name,
Config: data,
}, nil
}
// Expiry holds configuration for the validity period of components.
type Expiry struct {
// SigningKeys defines the duration of time after which the SigningKeys will be rotated.
SigningKeys string `json:"signingKeys"`
// IdTokens defines the duration of time for which the IdTokens will be valid.
IDTokens string `json:"idTokens"`
// AuthRequests defines the duration of time for which the AuthRequests will be valid.
AuthRequests string `json:"authRequests"`
// DeviceRequests defines the duration of time for which the DeviceRequests will be valid.
DeviceRequests string `json:"deviceRequests"`
// RefreshTokens defines refresh tokens expiry policy
RefreshTokens RefreshToken `json:"refreshTokens"`
}
// Logger holds configuration required to customize logging for dex.
type Logger struct {
// Level sets logging level severity.
Level string `json:"level"`
// Format specifies the format to be used for logging.
Format string `json:"format"`
}
type RefreshToken struct {
DisableRotation bool `json:"disableRotation"`
ReuseInterval string `json:"reuseInterval"`
AbsoluteLifetime string `json:"absoluteLifetime"`
ValidIfNotUsedFor string `json:"validIfNotUsedFor"`
}

View File

@ -1,425 +0,0 @@
package main
import (
"os"
"testing"
"github.com/ghodss/yaml"
"github.com/kylelemons/godebug/pretty"
"github.com/dexidp/dex/connector/mock"
"github.com/dexidp/dex/connector/oidc"
"github.com/dexidp/dex/server"
"github.com/dexidp/dex/storage"
"github.com/dexidp/dex/storage/sql"
)
var _ = yaml.YAMLToJSON
func TestValidConfiguration(t *testing.T) {
configuration := Config{
Issuer: "http://127.0.0.1:5556/dex",
Storage: Storage{
Type: "sqlite3",
Config: &sql.SQLite3{
File: "examples/dex.db",
},
},
Web: Web{
HTTP: "127.0.0.1:5556",
},
StaticConnectors: []Connector{
{
Type: "mockCallback",
ID: "mock",
Name: "Example",
Config: &mock.CallbackConfig{},
},
},
}
if err := configuration.Validate(); err != nil {
t.Fatalf("this configuration should have been valid: %v", err)
}
}
func TestInvalidConfiguration(t *testing.T) {
configuration := Config{}
err := configuration.Validate()
if err == nil {
t.Fatal("this configuration should be invalid")
}
got := err.Error()
wanted := `invalid Config:
- no issuer specified in config file
- no storage supplied in config file
- must supply a HTTP/HTTPS address to listen on`
if got != wanted {
t.Fatalf("Expected error message to be %q, got %q", wanted, got)
}
}
func TestUnmarshalConfig(t *testing.T) {
rawConfig := []byte(`
issuer: http://127.0.0.1:5556/dex
storage:
type: postgres
config:
host: 10.0.0.1
port: 65432
maxOpenConns: 5
maxIdleConns: 3
connMaxLifetime: 30
connectionTimeout: 3
web:
http: 127.0.0.1:5556
frontend:
dir: ./web
extra:
foo: bar
staticClients:
- id: example-app
redirectURIs:
- 'http://127.0.0.1:5555/callback'
name: 'Example App'
secret: ZXhhbXBsZS1hcHAtc2VjcmV0
oauth2:
alwaysShowLoginScreen: true
connectors:
- type: mockCallback
id: mock
name: Example
- type: oidc
id: google
name: Google
config:
issuer: https://accounts.google.com
clientID: foo
clientSecret: bar
redirectURI: http://127.0.0.1:5556/dex/callback/google
enablePasswordDB: true
staticPasswords:
- email: "admin@example.com"
# bcrypt hash of the string "password"
hash: "$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy"
username: "admin"
userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
- email: "foo@example.com"
# base64'd value of the same bcrypt hash above. We want to be able to parse both of these
hash: "JDJhJDEwJDMzRU1UMGNWWVZsUHk2V0FNQ0xzY2VMWWpXaHVIcGJ6NXl1Wnh1L0dBRmowM0o5THl0anV5"
username: "foo"
userID: "41331323-6f44-45e6-b3b9-2c4b60c02be5"
expiry:
signingKeys: "7h"
idTokens: "25h"
authRequests: "25h"
deviceRequests: "10m"
logger:
level: "debug"
format: "json"
`)
want := Config{
Issuer: "http://127.0.0.1:5556/dex",
Storage: Storage{
Type: "postgres",
Config: &sql.Postgres{
NetworkDB: sql.NetworkDB{
Host: "10.0.0.1",
Port: 65432,
MaxOpenConns: 5,
MaxIdleConns: 3,
ConnMaxLifetime: 30,
ConnectionTimeout: 3,
},
},
},
Web: Web{
HTTP: "127.0.0.1:5556",
},
Frontend: server.WebConfig{
Dir: "./web",
Extra: map[string]string{
"foo": "bar",
},
},
StaticClients: []storage.Client{
{
ID: "example-app",
Secret: "ZXhhbXBsZS1hcHAtc2VjcmV0",
Name: "Example App",
RedirectURIs: []string{
"http://127.0.0.1:5555/callback",
},
},
},
OAuth2: OAuth2{
AlwaysShowLoginScreen: true,
},
StaticConnectors: []Connector{
{
Type: "mockCallback",
ID: "mock",
Name: "Example",
Config: &mock.CallbackConfig{},
},
{
Type: "oidc",
ID: "google",
Name: "Google",
Config: &oidc.Config{
Issuer: "https://accounts.google.com",
ClientID: "foo",
ClientSecret: "bar",
RedirectURI: "http://127.0.0.1:5556/dex/callback/google",
},
},
},
EnablePasswordDB: true,
StaticPasswords: []password{
{
Email: "admin@example.com",
Hash: []byte("$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy"),
Username: "admin",
UserID: "08a8684b-db88-4b73-90a9-3cd1661f5466",
},
{
Email: "foo@example.com",
Hash: []byte("$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy"),
Username: "foo",
UserID: "41331323-6f44-45e6-b3b9-2c4b60c02be5",
},
},
Expiry: Expiry{
SigningKeys: "7h",
IDTokens: "25h",
AuthRequests: "25h",
DeviceRequests: "10m",
},
Logger: Logger{
Level: "debug",
Format: "json",
},
}
var c Config
if err := yaml.Unmarshal(rawConfig, &c); err != nil {
t.Fatalf("failed to decode config: %v", err)
}
if diff := pretty.Compare(c, want); diff != "" {
t.Errorf("got!=want: %s", diff)
}
}
func TestUnmarshalConfigWithEnvNoExpand(t *testing.T) {
// If the env variable DEX_EXPAND_ENV is set and has a "falsy" value, os.ExpandEnv is disabled.
// ParseBool: "It accepts 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False."
checkUnmarshalConfigWithEnv(t, "0", false)
checkUnmarshalConfigWithEnv(t, "f", false)
checkUnmarshalConfigWithEnv(t, "F", false)
checkUnmarshalConfigWithEnv(t, "FALSE", false)
checkUnmarshalConfigWithEnv(t, "false", false)
checkUnmarshalConfigWithEnv(t, "False", false)
os.Unsetenv("DEX_EXPAND_ENV")
}
func TestUnmarshalConfigWithEnvExpand(t *testing.T) {
// If the env variable DEX_EXPAND_ENV is unset or has a "truthy" or unknown value, os.ExpandEnv is enabled.
// ParseBool: "It accepts 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False."
checkUnmarshalConfigWithEnv(t, "1", true)
checkUnmarshalConfigWithEnv(t, "t", true)
checkUnmarshalConfigWithEnv(t, "T", true)
checkUnmarshalConfigWithEnv(t, "TRUE", true)
checkUnmarshalConfigWithEnv(t, "true", true)
checkUnmarshalConfigWithEnv(t, "True", true)
// Values that can't be parsed as bool:
checkUnmarshalConfigWithEnv(t, "UNSET", true)
checkUnmarshalConfigWithEnv(t, "", true)
checkUnmarshalConfigWithEnv(t, "whatever - true is default", true)
os.Unsetenv("DEX_EXPAND_ENV")
}
func checkUnmarshalConfigWithEnv(t *testing.T, dexExpandEnv string, wantExpandEnv bool) {
// For hashFromEnv:
os.Setenv("DEX_FOO_USER_PASSWORD", "$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy")
// For os.ExpandEnv ($VAR -> value_of_VAR):
os.Setenv("DEX_FOO_POSTGRES_HOST", "10.0.0.1")
os.Setenv("DEX_FOO_OIDC_CLIENT_SECRET", "bar")
if dexExpandEnv != "UNSET" {
os.Setenv("DEX_EXPAND_ENV", dexExpandEnv)
} else {
os.Unsetenv("DEX_EXPAND_ENV")
}
rawConfig := []byte(`
issuer: http://127.0.0.1:5556/dex
storage:
type: postgres
config:
# Env variables are expanded in raw YAML source.
# Single quotes work fine, as long as the env variable doesn't contain any.
host: '$DEX_FOO_POSTGRES_HOST'
port: 65432
maxOpenConns: 5
maxIdleConns: 3
connMaxLifetime: 30
connectionTimeout: 3
web:
http: 127.0.0.1:5556
frontend:
dir: ./web
extra:
foo: bar
staticClients:
- id: example-app
redirectURIs:
- 'http://127.0.0.1:5555/callback'
name: 'Example App'
secret: ZXhhbXBsZS1hcHAtc2VjcmV0
oauth2:
alwaysShowLoginScreen: true
connectors:
- type: mockCallback
id: mock
name: Example
- type: oidc
id: google
name: Google
config:
issuer: https://accounts.google.com
clientID: foo
# Env variables are expanded in raw YAML source.
# Single quotes work fine, as long as the env variable doesn't contain any.
clientSecret: '$DEX_FOO_OIDC_CLIENT_SECRET'
redirectURI: http://127.0.0.1:5556/dex/callback/google
enablePasswordDB: true
staticPasswords:
- email: "admin@example.com"
# bcrypt hash of the string "password"
hash: "$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy"
username: "admin"
userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
- email: "foo@example.com"
hashFromEnv: "DEX_FOO_USER_PASSWORD"
username: "foo"
userID: "41331323-6f44-45e6-b3b9-2c4b60c02be5"
expiry:
signingKeys: "7h"
idTokens: "25h"
authRequests: "25h"
logger:
level: "debug"
format: "json"
`)
// This is not a valid hostname. It's only used to check whether os.ExpandEnv was applied or not.
wantPostgresHost := "$DEX_FOO_POSTGRES_HOST"
wantOidcClientSecret := "$DEX_FOO_OIDC_CLIENT_SECRET"
if wantExpandEnv {
wantPostgresHost = "10.0.0.1"
wantOidcClientSecret = "bar"
}
want := Config{
Issuer: "http://127.0.0.1:5556/dex",
Storage: Storage{
Type: "postgres",
Config: &sql.Postgres{
NetworkDB: sql.NetworkDB{
Host: wantPostgresHost,
Port: 65432,
MaxOpenConns: 5,
MaxIdleConns: 3,
ConnMaxLifetime: 30,
ConnectionTimeout: 3,
},
},
},
Web: Web{
HTTP: "127.0.0.1:5556",
},
Frontend: server.WebConfig{
Dir: "./web",
Extra: map[string]string{
"foo": "bar",
},
},
StaticClients: []storage.Client{
{
ID: "example-app",
Secret: "ZXhhbXBsZS1hcHAtc2VjcmV0",
Name: "Example App",
RedirectURIs: []string{
"http://127.0.0.1:5555/callback",
},
},
},
OAuth2: OAuth2{
AlwaysShowLoginScreen: true,
},
StaticConnectors: []Connector{
{
Type: "mockCallback",
ID: "mock",
Name: "Example",
Config: &mock.CallbackConfig{},
},
{
Type: "oidc",
ID: "google",
Name: "Google",
Config: &oidc.Config{
Issuer: "https://accounts.google.com",
ClientID: "foo",
ClientSecret: wantOidcClientSecret,
RedirectURI: "http://127.0.0.1:5556/dex/callback/google",
},
},
},
EnablePasswordDB: true,
StaticPasswords: []password{
{
Email: "admin@example.com",
Hash: []byte("$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy"),
Username: "admin",
UserID: "08a8684b-db88-4b73-90a9-3cd1661f5466",
},
{
Email: "foo@example.com",
Hash: []byte("$2a$10$33EMT0cVYVlPy6WAMCLsceLYjWhuHpbz5yuZxu/GAFj03J9Lytjuy"),
Username: "foo",
UserID: "41331323-6f44-45e6-b3b9-2c4b60c02be5",
},
},
Expiry: Expiry{
SigningKeys: "7h",
IDTokens: "25h",
AuthRequests: "25h",
},
Logger: Logger{
Level: "debug",
Format: "json",
},
}
var c Config
if err := yaml.Unmarshal(rawConfig, &c); err != nil {
t.Fatalf("failed to decode config: %v", err)
}
if diff := pretty.Compare(c, want); diff != "" {
t.Errorf("got!=want: %s", diff)
}
}

View File

@ -1,28 +0,0 @@
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
func commandRoot() *cobra.Command {
rootCmd := &cobra.Command{
Use: "dex",
Run: func(cmd *cobra.Command, args []string) {
cmd.Help()
os.Exit(2)
},
}
rootCmd.AddCommand(commandServe())
rootCmd.AddCommand(commandVersion())
return rootCmd
}
func main() {
if err := commandRoot().Execute(); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(2)
}
}

View File

@ -1,565 +0,0 @@
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"net/http"
"net/http/pprof"
"os"
"runtime"
"strings"
"syscall"
"time"
gosundheit "github.com/AppsFlyer/go-sundheit"
"github.com/AppsFlyer/go-sundheit/checks"
gosundheithttp "github.com/AppsFlyer/go-sundheit/http"
"github.com/ghodss/yaml"
grpcprometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
"github.com/oklog/run"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/reflection"
"github.com/dexidp/dex/api/v2"
"github.com/dexidp/dex/pkg/log"
"github.com/dexidp/dex/server"
"github.com/dexidp/dex/storage"
)
type serveOptions struct {
// Config file path
config string
// Flags
webHTTPAddr string
webHTTPSAddr string
telemetryAddr string
grpcAddr string
}
func commandServe() *cobra.Command {
options := serveOptions{}
cmd := &cobra.Command{
Use: "serve [flags] [config file]",
Short: "Launch Dex",
Example: "dex serve config.yaml",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
cmd.SilenceErrors = true
options.config = args[0]
return runServe(options)
},
}
flags := cmd.Flags()
flags.StringVar(&options.webHTTPAddr, "web-http-addr", "", "Web HTTP address")
flags.StringVar(&options.webHTTPSAddr, "web-https-addr", "", "Web HTTPS address")
flags.StringVar(&options.telemetryAddr, "telemetry-addr", "", "Telemetry address")
flags.StringVar(&options.grpcAddr, "grpc-addr", "", "gRPC API address")
return cmd
}
func runServe(options serveOptions) error {
configFile := options.config
configData, err := os.ReadFile(configFile)
if err != nil {
return fmt.Errorf("failed to read config file %s: %v", configFile, err)
}
var c Config
if err := yaml.Unmarshal(configData, &c); err != nil {
return fmt.Errorf("error parse config file %s: %v", configFile, err)
}
applyConfigOverrides(options, &c)
logger, err := newLogger(c.Logger.Level, c.Logger.Format)
if err != nil {
return fmt.Errorf("invalid config: %v", err)
}
logger.Infof(
"Dex Version: %s, Go Version: %s, Go OS/ARCH: %s %s",
version,
runtime.Version(),
runtime.GOOS,
runtime.GOARCH,
)
if c.Logger.Level != "" {
logger.Infof("config using log level: %s", c.Logger.Level)
}
if err := c.Validate(); err != nil {
return err
}
logger.Infof("config issuer: %s", c.Issuer)
prometheusRegistry := prometheus.NewRegistry()
err = prometheusRegistry.Register(collectors.NewGoCollector())
if err != nil {
return fmt.Errorf("failed to register Go runtime metrics: %v", err)
}
err = prometheusRegistry.Register(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
if err != nil {
return fmt.Errorf("failed to register process metrics: %v", err)
}
grpcMetrics := grpcprometheus.NewServerMetrics()
err = prometheusRegistry.Register(grpcMetrics)
if err != nil {
return fmt.Errorf("failed to register gRPC server metrics: %v", err)
}
var grpcOptions []grpc.ServerOption
allowedTLSCiphers := []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
}
if c.GRPC.TLSCert != "" {
// Parse certificates from certificate file and key file for server.
cert, err := tls.LoadX509KeyPair(c.GRPC.TLSCert, c.GRPC.TLSKey)
if err != nil {
return fmt.Errorf("invalid config: error parsing gRPC certificate file: %v", err)
}
tlsConfig := tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
CipherSuites: allowedTLSCiphers,
PreferServerCipherSuites: true,
}
if c.GRPC.TLSClientCA != "" {
// Parse certificates from client CA file to a new CertPool.
cPool := x509.NewCertPool()
clientCert, err := os.ReadFile(c.GRPC.TLSClientCA)
if err != nil {
return fmt.Errorf("invalid config: reading from client CA file: %v", err)
}
if !cPool.AppendCertsFromPEM(clientCert) {
return errors.New("invalid config: failed to parse client CA")
}
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
tlsConfig.ClientCAs = cPool
// Only add metrics if client auth is enabled
grpcOptions = append(grpcOptions,
grpc.StreamInterceptor(grpcMetrics.StreamServerInterceptor()),
grpc.UnaryInterceptor(grpcMetrics.UnaryServerInterceptor()),
)
}
grpcOptions = append(grpcOptions, grpc.Creds(credentials.NewTLS(&tlsConfig)))
}
s, err := c.Storage.Config.Open(logger)
if err != nil {
return fmt.Errorf("failed to initialize storage: %v", err)
}
defer s.Close()
logger.Infof("config storage: %s", c.Storage.Type)
if len(c.StaticClients) > 0 {
for i, client := range c.StaticClients {
if client.Name == "" {
return fmt.Errorf("invalid config: Name field is required for a client")
}
if client.ID == "" && client.IDEnv == "" {
return fmt.Errorf("invalid config: ID or IDEnv field is required for a client")
}
if client.IDEnv != "" {
if client.ID != "" {
return fmt.Errorf("invalid config: ID and IDEnv fields are exclusive for client %q", client.ID)
}
c.StaticClients[i].ID = os.Getenv(client.IDEnv)
}
if client.Secret == "" && client.SecretEnv == "" && !client.Public {
return fmt.Errorf("invalid config: Secret or SecretEnv field is required for client %q", client.ID)
}
if client.SecretEnv != "" {
if client.Secret != "" {
return fmt.Errorf("invalid config: Secret and SecretEnv fields are exclusive for client %q", client.ID)
}
c.StaticClients[i].Secret = os.Getenv(client.SecretEnv)
}
logger.Infof("config static client: %s", client.Name)
}
s = storage.WithStaticClients(s, c.StaticClients)
}
if len(c.StaticPasswords) > 0 {
passwords := make([]storage.Password, len(c.StaticPasswords))
for i, p := range c.StaticPasswords {
passwords[i] = storage.Password(p)
}
s = storage.WithStaticPasswords(s, passwords, logger)
}
storageConnectors := make([]storage.Connector, len(c.StaticConnectors))
for i, c := range c.StaticConnectors {
if c.ID == "" || c.Name == "" || c.Type == "" {
return fmt.Errorf("invalid config: ID, Type and Name fields are required for a connector")
}
if c.Config == nil {
return fmt.Errorf("invalid config: no config field for connector %q", c.ID)
}
logger.Infof("config connector: %s", c.ID)
// convert to a storage connector object
conn, err := ToStorageConnector(c)
if err != nil {
return fmt.Errorf("failed to initialize storage connectors: %v", err)
}
storageConnectors[i] = conn
}
if c.EnablePasswordDB {
storageConnectors = append(storageConnectors, storage.Connector{
ID: server.LocalConnector,
Name: "Email",
Type: server.LocalConnector,
})
logger.Infof("config connector: local passwords enabled")
}
s = storage.WithStaticConnectors(s, storageConnectors)
if len(c.OAuth2.ResponseTypes) > 0 {
logger.Infof("config response types accepted: %s", c.OAuth2.ResponseTypes)
}
if c.OAuth2.SkipApprovalScreen {
logger.Infof("config skipping approval screen")
}
if c.OAuth2.PasswordConnector != "" {
logger.Infof("config using password grant connector: %s", c.OAuth2.PasswordConnector)
}
if len(c.Web.AllowedOrigins) > 0 {
logger.Infof("config allowed origins: %s", c.Web.AllowedOrigins)
}
// explicitly convert to UTC.
now := func() time.Time { return time.Now().UTC() }
healthChecker := gosundheit.New()
serverConfig := server.Config{
SupportedResponseTypes: c.OAuth2.ResponseTypes,
SkipApprovalScreen: c.OAuth2.SkipApprovalScreen,
AlwaysShowLoginScreen: c.OAuth2.AlwaysShowLoginScreen,
PasswordConnector: c.OAuth2.PasswordConnector,
AllowedOrigins: c.Web.AllowedOrigins,
Issuer: c.Issuer,
Storage: s,
Web: c.Frontend,
Logger: logger,
Now: now,
PrometheusRegistry: prometheusRegistry,
HealthChecker: healthChecker,
}
if c.Expiry.SigningKeys != "" {
signingKeys, err := time.ParseDuration(c.Expiry.SigningKeys)
if err != nil {
return fmt.Errorf("invalid config value %q for signing keys expiry: %v", c.Expiry.SigningKeys, err)
}
logger.Infof("config signing keys expire after: %v", signingKeys)
serverConfig.RotateKeysAfter = signingKeys
}
if c.Expiry.IDTokens != "" {
idTokens, err := time.ParseDuration(c.Expiry.IDTokens)
if err != nil {
return fmt.Errorf("invalid config value %q for id token expiry: %v", c.Expiry.IDTokens, err)
}
logger.Infof("config id tokens valid for: %v", idTokens)
serverConfig.IDTokensValidFor = idTokens
}
if c.Expiry.AuthRequests != "" {
authRequests, err := time.ParseDuration(c.Expiry.AuthRequests)
if err != nil {
return fmt.Errorf("invalid config value %q for auth request expiry: %v", c.Expiry.AuthRequests, err)
}
logger.Infof("config auth requests valid for: %v", authRequests)
serverConfig.AuthRequestsValidFor = authRequests
}
if c.Expiry.DeviceRequests != "" {
deviceRequests, err := time.ParseDuration(c.Expiry.DeviceRequests)
if err != nil {
return fmt.Errorf("invalid config value %q for device request expiry: %v", c.Expiry.AuthRequests, err)
}
logger.Infof("config device requests valid for: %v", deviceRequests)
serverConfig.DeviceRequestsValidFor = deviceRequests
}
refreshTokenPolicy, err := server.NewRefreshTokenPolicy(
logger,
c.Expiry.RefreshTokens.DisableRotation,
c.Expiry.RefreshTokens.ValidIfNotUsedFor,
c.Expiry.RefreshTokens.AbsoluteLifetime,
c.Expiry.RefreshTokens.ReuseInterval,
)
if err != nil {
return fmt.Errorf("invalid refresh token expiration policy config: %v", err)
}
serverConfig.RefreshTokenPolicy = refreshTokenPolicy
serv, err := server.NewServer(context.Background(), serverConfig)
if err != nil {
return fmt.Errorf("failed to initialize server: %v", err)
}
telemetryRouter := http.NewServeMux()
telemetryRouter.Handle("/metrics", promhttp.HandlerFor(prometheusRegistry, promhttp.HandlerOpts{}))
// Configure health checker
{
handler := gosundheithttp.HandleHealthJSON(healthChecker)
telemetryRouter.Handle("/healthz", handler)
// Kubernetes style health checks
telemetryRouter.HandleFunc("/healthz/live", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("ok"))
})
telemetryRouter.Handle("/healthz/ready", handler)
}
healthChecker.RegisterCheck(
&checks.CustomCheck{
CheckName: "storage",
CheckFunc: storage.NewCustomHealthCheckFunc(serverConfig.Storage, serverConfig.Now),
},
gosundheit.ExecutionPeriod(15*time.Second),
gosundheit.InitiallyPassing(true),
)
var group run.Group
// Set up telemetry server
if c.Telemetry.HTTP != "" {
const name = "telemetry"
logger.Infof("listening (%s) on %s", name, c.Telemetry.HTTP)
l, err := net.Listen("tcp", c.Telemetry.HTTP)
if err != nil {
return fmt.Errorf("listening (%s) on %s: %v", name, c.Telemetry.HTTP, err)
}
if c.Telemetry.EnableProfiling {
pprofHandler(telemetryRouter)
}
server := &http.Server{
Handler: telemetryRouter,
}
defer server.Close()
group.Add(func() error {
return server.Serve(l)
}, func(err error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
logger.Debugf("starting graceful shutdown (%s)", name)
if err := server.Shutdown(ctx); err != nil {
logger.Errorf("graceful shutdown (%s): %v", name, err)
}
})
}
// Set up http server
if c.Web.HTTP != "" {
const name = "http"
logger.Infof("listening (%s) on %s", name, c.Web.HTTP)
l, err := net.Listen("tcp", c.Web.HTTP)
if err != nil {
return fmt.Errorf("listening (%s) on %s: %v", name, c.Web.HTTP, err)
}
server := &http.Server{
Handler: serv,
}
defer server.Close()
group.Add(func() error {
return server.Serve(l)
}, func(err error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
logger.Debugf("starting graceful shutdown (%s)", name)
if err := server.Shutdown(ctx); err != nil {
logger.Errorf("graceful shutdown (%s): %v", name, err)
}
})
}
// Set up https server
if c.Web.HTTPS != "" {
const name = "https"
logger.Infof("listening (%s) on %s", name, c.Web.HTTPS)
l, err := net.Listen("tcp", c.Web.HTTPS)
if err != nil {
return fmt.Errorf("listening (%s) on %s: %v", name, c.Web.HTTPS, err)
}
server := &http.Server{
Handler: serv,
TLSConfig: &tls.Config{
CipherSuites: allowedTLSCiphers,
PreferServerCipherSuites: true,
MinVersion: tls.VersionTLS12,
},
}
defer server.Close()
group.Add(func() error {
return server.ServeTLS(l, c.Web.TLSCert, c.Web.TLSKey)
}, func(err error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
logger.Debugf("starting graceful shutdown (%s)", name)
if err := server.Shutdown(ctx); err != nil {
logger.Errorf("graceful shutdown (%s): %v", name, err)
}
})
}
// Set up grpc server
if c.GRPC.Addr != "" {
logger.Infof("listening (grpc) on %s", c.GRPC.Addr)
grpcListener, err := net.Listen("tcp", c.GRPC.Addr)
if err != nil {
return fmt.Errorf("listening (grcp) on %s: %w", c.GRPC.Addr, err)
}
grpcSrv := grpc.NewServer(grpcOptions...)
api.RegisterDexServer(grpcSrv, server.NewAPI(serverConfig.Storage, logger, version))
grpcMetrics.InitializeMetrics(grpcSrv)
if c.GRPC.Reflection {
logger.Info("enabling reflection in grpc service")
reflection.Register(grpcSrv)
}
group.Add(func() error {
return grpcSrv.Serve(grpcListener)
}, func(err error) {
logger.Debugf("starting graceful shutdown (grpc)")
grpcSrv.GracefulStop()
})
}
group.Add(run.SignalHandler(context.Background(), os.Interrupt, syscall.SIGTERM))
if err := group.Run(); err != nil {
if _, ok := err.(run.SignalError); !ok {
return fmt.Errorf("run groups: %w", err)
}
logger.Infof("%v, shutdown now", err)
}
return nil
}
var (
logLevels = []string{"debug", "info", "error"}
logFormats = []string{"json", "text"}
)
type utcFormatter struct {
f logrus.Formatter
}
func (f *utcFormatter) Format(e *logrus.Entry) ([]byte, error) {
e.Time = e.Time.UTC()
return f.f.Format(e)
}
func newLogger(level string, format string) (log.Logger, error) {
var logLevel logrus.Level
switch strings.ToLower(level) {
case "debug":
logLevel = logrus.DebugLevel
case "", "info":
logLevel = logrus.InfoLevel
case "error":
logLevel = logrus.ErrorLevel
default:
return nil, fmt.Errorf("log level is not one of the supported values (%s): %s", strings.Join(logLevels, ", "), level)
}
var formatter utcFormatter
switch strings.ToLower(format) {
case "", "text":
formatter.f = &logrus.TextFormatter{DisableColors: true}
case "json":
formatter.f = &logrus.JSONFormatter{}
default:
return nil, fmt.Errorf("log format is not one of the supported values (%s): %s", strings.Join(logFormats, ", "), format)
}
return &logrus.Logger{
Out: os.Stderr,
Formatter: &formatter,
Level: logLevel,
}, nil
}
func applyConfigOverrides(options serveOptions, config *Config) {
if options.webHTTPAddr != "" {
config.Web.HTTP = options.webHTTPAddr
}
if options.webHTTPSAddr != "" {
config.Web.HTTPS = options.webHTTPSAddr
}
if options.telemetryAddr != "" {
config.Telemetry.HTTP = options.telemetryAddr
}
if options.grpcAddr != "" {
config.GRPC.Addr = options.grpcAddr
}
if config.Frontend.Dir == "" {
config.Frontend.Dir = os.Getenv("DEX_FRONTEND_DIR")
}
}
func pprofHandler(router *http.ServeMux) {
router.HandleFunc("/debug/pprof/", pprof.Index)
router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
router.HandleFunc("/debug/pprof/profile", pprof.Profile)
router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
router.HandleFunc("/debug/pprof/trace", pprof.Trace)
}

View File

@ -1,26 +0,0 @@
package main
import (
"fmt"
"runtime"
"github.com/spf13/cobra"
)
var version = "DEV"
func commandVersion() *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Print the version and exit",
Run: func(_ *cobra.Command, _ []string) {
fmt.Printf(
"Dex Version: %s\nGo Version: %s\nGo OS/ARCH: %s %s\n",
version,
runtime.Version(),
runtime.GOOS,
runtime.GOARCH,
)
},
}
}

View File

@ -0,0 +1,54 @@
package main
import (
"net/url"
"github.com/coreos/go-oidc/oidc"
"github.com/spf13/cobra"
)
var (
cmdNewClient = &cobra.Command{
Use: "new-client",
Short: "Create a new client with one or more redirect URLs.",
Long: "Create a new client with one or more redirect URLs,",
Example: ` dexctl new-client --db-url=${DB_URL} 'https://example.com/callback'`,
Run: wrapRun(runNewClient),
}
)
func init() {
rootCmd.AddCommand(cmdNewClient)
}
func runNewClient(cmd *cobra.Command, args []string) int {
if len(args) < 1 {
stderr("Provide at least one redirect URL.")
return 2
}
redirectURLs := make([]url.URL, len(args))
for i, ua := range args {
u, err := url.Parse(ua)
if err != nil {
stderr("Malformed URL %q: %v", ua, err)
return 1
}
redirectURLs[i] = *u
}
cc, err := getDriver().NewClient(oidc.ClientMetadata{RedirectURIs: redirectURLs})
if err != nil {
stderr("Failed creating new client: %v", err)
return 1
}
stdout("# Added new client:")
stdout("DEX_APP_CLIENT_ID=%s", cc.ID)
stdout("DEX_APP_CLIENT_SECRET=%s", cc.Secret)
for i, u := range redirectURLs {
stdout("DEX_APP_REDIRECTURL_%d=%s", i, u.String())
}
return 0
}

View File

@ -0,0 +1,90 @@
package main
import (
"fmt"
"io"
"os"
"github.com/coreos/dex/connector"
"github.com/spf13/cobra"
)
var (
cmdGetConnectorConfigs = &cobra.Command{
Use: "get-connector-configs",
Short: "Enumerate current IdP connector configs.",
Long: "Enumerate current IdP connector configs.",
Example: ` dexctl get-connector-configs --db-url=${DB_URL}`,
Run: wrapRun(runGetConnectorConfigs),
}
cmdSetConnectorConfigs = &cobra.Command{
Use: "set-connector-configs",
Short: "Overwrite the current IdP connector configs with those from a local file. Provide the argument '-' to read from stdin.",
Long: "Overwrite the current IdP connector configs with those from a local file. Provide the argument '-' to read from stdin.",
Example: ` dexctl set-connector-configs --db-url=${DB_URL} ./static/conn_conf.json`,
Run: wrapRun(runSetConnectorConfigs),
}
)
func init() {
rootCmd.AddCommand(cmdGetConnectorConfigs)
rootCmd.AddCommand(cmdSetConnectorConfigs)
}
func runSetConnectorConfigs(cmd *cobra.Command, args []string) int {
if len(args) != 1 {
stderr("Provide a single argument.")
return 2
}
var r io.Reader
if from := args[0]; from == "-" {
r = os.Stdin
} else {
f, err := os.Open(from)
if err != nil {
stderr("Unable to open specified file: %v", err)
return 1
}
defer f.Close()
r = f
}
cfgs, err := connector.ReadConfigs(r)
if err != nil {
stderr("Unable to decode connector configs: %v", err)
return 1
}
if err := getDriver().SetConnectorConfigs(cfgs); err != nil {
stderr(err.Error())
return 1
}
fmt.Printf("Saved %d connector config(s)\n", len(cfgs))
return 0
}
func runGetConnectorConfigs(cmd *cobra.Command, args []string) int {
if len(args) != 0 {
stderr("Provide zero arguments.")
return 2
}
cfgs, err := getDriver().ConnectorConfigs()
if err != nil {
stderr("Unable to retrieve connector configs: %v", err)
return 1
}
fmt.Printf("Found %d connector config(s)\n", len(cfgs))
for _, cfg := range cfgs {
fmt.Println()
fmt.Printf("ID: %v\n", cfg.ConnectorID())
fmt.Printf("Type: %v\n", cfg.ConnectorType())
}
return 0
}

View File

@ -0,0 +1,26 @@
package main
import (
"runtime"
"strings"
"github.com/spf13/cobra"
)
var (
// set by the top level build script
version = ""
cmdVersion = &cobra.Command{
Use: "version",
Short: "Print the dexctl version.",
Long: "Print the dexctl version.",
Run: func(cmd *cobra.Command, args []string) {
stdout("dex version %s\ngo version %s", strings.TrimPrefix(version, "v"), strings.TrimPrefix(runtime.Version(), "go"))
},
}
)
func init() {
rootCmd.AddCommand(cmdVersion)
}

13
cmd/dexctl/driver.go Normal file
View File

@ -0,0 +1,13 @@
package main
import (
"github.com/coreos/dex/connector"
"github.com/coreos/go-oidc/oidc"
)
type driver interface {
NewClient(oidc.ClientMetadata) (*oidc.ClientCredentials, error)
ConnectorConfigs() ([]connector.ConnectorConfig, error)
SetConnectorConfigs([]connector.ConnectorConfig) error
}

46
cmd/dexctl/driver_db.go Normal file
View File

@ -0,0 +1,46 @@
package main
import (
"github.com/coreos/dex/client"
"github.com/coreos/dex/client/manager"
"github.com/coreos/dex/connector"
"github.com/coreos/dex/db"
"github.com/coreos/go-oidc/oidc"
)
func newDBDriver(dsn string) (driver, error) {
dbc, err := db.NewConnection(db.Config{DSN: dsn})
if err != nil {
return nil, err
}
drv := &dbDriver{
cfgRepo: db.NewConnectorConfigRepo(dbc),
ciManager: manager.NewClientManager(db.NewClientRepo(dbc), db.TransactionFactory(dbc), manager.ManagerOptions{}),
}
return drv, nil
}
type dbDriver struct {
ciManager *manager.ClientManager
cfgRepo *db.ConnectorConfigRepo
}
func (d *dbDriver) NewClient(meta oidc.ClientMetadata) (*oidc.ClientCredentials, error) {
if err := meta.Valid(); err != nil {
return nil, err
}
cli := client.Client{
Metadata: meta,
}
return d.ciManager.New(cli, nil)
}
func (d *dbDriver) ConnectorConfigs() ([]connector.ConnectorConfig, error) {
return d.cfgRepo.All()
}
func (d *dbDriver) SetConnectorConfigs(cfgs []connector.ConnectorConfig) error {
return d.cfgRepo.Set(cfgs)
}

87
cmd/dexctl/main.go Normal file
View File

@ -0,0 +1,87 @@
package main
import (
"errors"
"os"
"strings"
"github.com/coreos/dex/pkg/log"
"github.com/coreos/go-oidc/oidc"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
var (
rootCmd = &cobra.Command{
Use: "dexctl",
Short: "A command line tool for interacting with the dex system",
Long: "",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// initialize flags from environment
fs := cmd.Flags()
// don't override flags set by command line flags
alreadySet := make(map[string]bool)
fs.Visit(func(f *pflag.Flag) { alreadySet[f.Name] = true })
var err error
fs.VisitAll(func(f *pflag.Flag) {
if err != nil || alreadySet[f.Name] {
return
}
key := "DEXCTL_" + strings.ToUpper(strings.Replace(f.Name, "-", "_", -1))
if val := os.Getenv(key); val != "" {
err = fs.Set(f.Name, val)
}
})
return err
},
Run: func(cmd *cobra.Command, args []string) {
cmd.Help()
os.Exit(2)
},
}
global struct {
creds oidc.ClientCredentials
dbURL string
help bool
logDebug bool
}
)
func init() {
log.EnableTimestamps()
rootCmd.PersistentFlags().StringVar(&global.dbURL, "db-url", "", "DSN-formatted database connection string")
rootCmd.PersistentFlags().BoolVar(&global.logDebug, "log-debug", false, "Log debug-level information")
}
func main() {
if err := rootCmd.Execute(); err != nil {
os.Exit(2)
}
}
func wrapRun(run func(cmd *cobra.Command, args []string) int) func(cmd *cobra.Command, args []string) {
return func(cmd *cobra.Command, args []string) {
os.Exit(run(cmd, args))
}
}
func getDriver() (drv driver) {
var err error
switch {
case len(global.dbURL) > 0:
drv, err = newDBDriver(global.dbURL)
default:
err = errors.New("--db-url flag unset")
}
if err != nil {
stderr("Unable to configure dexctl driver: %v", err)
os.Exit(1)
}
return
}

21
cmd/dexctl/util.go Normal file
View File

@ -0,0 +1,21 @@
package main
import (
"fmt"
"os"
"strings"
)
func stderr(format string, args ...interface{}) {
if !strings.HasSuffix(format, "\n") {
format = format + "\n"
}
fmt.Fprintf(os.Stderr, format, args...)
}
func stdout(format string, args ...interface{}) {
if !strings.HasSuffix(format, "\n") {
format = format + "\n"
}
fmt.Fprintf(os.Stdout, format, args...)
}

View File

@ -1,92 +0,0 @@
// Package main provides a utility program to launch the Dex container process with an optional
// templating step (provided by gomplate).
//
// This was originally written as a shell script, but we rewrote it as a Go program so that it could
// run as a raw binary in a distroless container.
package main
import (
"fmt"
"os"
"os/exec"
"strings"
"syscall"
)
func main() {
// Note that this docker-entrypoint program is args[0], and it is provided with the true process
// args.
args := os.Args[1:]
if err := run(args, realExec, realWhich); err != nil {
fmt.Println("error:", err.Error())
os.Exit(1)
}
}
func realExec(fork bool, args ...string) error {
if fork {
if output, err := exec.Command(args[0], args[1:]...).CombinedOutput(); err != nil {
return fmt.Errorf("cannot fork/exec command %s: %w (output: %q)", args, err, string(output))
}
return nil
}
argv0, err := exec.LookPath(args[0])
if err != nil {
return fmt.Errorf("cannot lookup path for command %s: %w", args[0], err)
}
if err := syscall.Exec(argv0, args, os.Environ()); err != nil {
return fmt.Errorf("cannot exec command %s (%q): %w", args, argv0, err)
}
return nil
}
func realWhich(path string) string {
fullPath, err := exec.LookPath(path)
if err != nil {
return ""
}
return fullPath
}
func run(args []string, execFunc func(bool, ...string) error, whichFunc func(string) string) error {
if args[0] != "dex" && args[0] != whichFunc("dex") {
return execFunc(false, args...)
}
if args[1] != "serve" {
return execFunc(false, args...)
}
newArgs := []string{}
for _, tplCandidate := range args {
if hasSuffixes(tplCandidate, ".tpl", ".tmpl", ".yaml") {
tmpFile, err := os.CreateTemp("/tmp", "dex.config.yaml-*")
if err != nil {
return fmt.Errorf("cannot create temp file: %w", err)
}
if err := execFunc(true, "gomplate", "-f", tplCandidate, "-o", tmpFile.Name()); err != nil {
return err
}
newArgs = append(newArgs, tmpFile.Name())
} else {
newArgs = append(newArgs, tplCandidate)
}
}
return execFunc(false, newArgs...)
}
func hasSuffixes(s string, suffixes ...string) bool {
for _, suffix := range suffixes {
if strings.HasSuffix(s, suffix) {
return true
}
}
return false
}

View File

@ -1,113 +0,0 @@
package main
import (
"strings"
"testing"
)
type execArgs struct {
fork bool
argPrefixes []string
}
func TestRun(t *testing.T) {
tests := []struct {
name string
args []string
execReturns error
whichReturns string
wantExecArgs []execArgs
wantErr error
}{
{
name: "executable not dex",
args: []string{"tuna", "fish"},
wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"tuna", "fish"}}},
},
{
name: "executable is full path to dex",
args: []string{"/usr/local/bin/dex", "marshmallow", "zelda"},
whichReturns: "/usr/local/bin/dex",
wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"/usr/local/bin/dex", "marshmallow", "zelda"}}},
},
{
name: "command is not serve",
args: []string{"dex", "marshmallow", "zelda"},
wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"dex", "marshmallow", "zelda"}}},
},
{
name: "no templates",
args: []string{"dex", "serve", "config.yaml.not-a-template"},
wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"dex", "serve", "config.yaml.not-a-template"}}},
},
{
name: "no templates",
args: []string{"dex", "serve", "config.yaml.not-a-template"},
wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"dex", "serve", "config.yaml.not-a-template"}}},
},
{
name: ".tpl template",
args: []string{"dex", "serve", "config.tpl"},
wantExecArgs: []execArgs{
{fork: true, argPrefixes: []string{"gomplate", "-f", "config.tpl", "-o", "/tmp/dex.config.yaml-"}},
{fork: false, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}},
},
},
{
name: ".tmpl template",
args: []string{"dex", "serve", "config.tmpl"},
wantExecArgs: []execArgs{
{fork: true, argPrefixes: []string{"gomplate", "-f", "config.tmpl", "-o", "/tmp/dex.config.yaml-"}},
{fork: false, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}},
},
},
{
name: ".yaml template",
args: []string{"dex", "serve", "some/path/config.yaml"},
wantExecArgs: []execArgs{
{fork: true, argPrefixes: []string{"gomplate", "-f", "some/path/config.yaml", "-o", "/tmp/dex.config.yaml-"}},
{fork: false, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var gotExecForks []bool
var gotExecArgs [][]string
fakeExec := func(fork bool, args ...string) error {
gotExecForks = append(gotExecForks, fork)
gotExecArgs = append(gotExecArgs, args)
return test.execReturns
}
fakeWhich := func(_ string) string { return test.whichReturns }
gotErr := run(test.args, fakeExec, fakeWhich)
if (test.wantErr == nil) != (gotErr == nil) {
t.Errorf("wanted error %s, got %s", test.wantErr, gotErr)
}
if !execArgsMatch(test.wantExecArgs, gotExecForks, gotExecArgs) {
t.Errorf("wanted exec args %+v, got %+v %+v", test.wantExecArgs, gotExecForks, gotExecArgs)
}
})
}
}
func execArgsMatch(wantExecArgs []execArgs, gotForks []bool, gotExecArgs [][]string) bool {
if len(wantExecArgs) != len(gotForks) {
return false
}
for i := range wantExecArgs {
if wantExecArgs[i].fork != gotForks[i] {
return false
}
for j := range wantExecArgs[i].argPrefixes {
if !strings.HasPrefix(gotExecArgs[i][j], wantExecArgs[i].argPrefixes[j]) {
return false
}
}
}
return true
}

41
cmd/genconfig/README.md Normal file
View File

@ -0,0 +1,41 @@
genconfig
====
The genconfig tool generates boilerplate code for registering and deserializing configuration objects.
## Usage - Go source
Let's say you have an interface called `Foo` with several concrete implmentations, and you'd like to be able to instantiate these implementations by reading JSON configuration files.
The first thing you do is create a new interface called `FooConfig` and add a `go:generate` directive to it like so:
//go:generate genconfig -o config.go foo Foo
type FooConfig interface {
FooID() string
Foo()
...
The first argument is the name of the file to create. The second (`foo`) is the name of the package. The third is the object that is to be configurable.
## Usage - Go Generate
To generate (or re-generate) your config file, issue the following command in your shell:
```
go generate github.com/coreos/dex/foo
```
## Usage - Generated Code API
Every time you make a new `Foo` implementation, you should create a new `FooConfig` which configures it, and can return a `Foo`. You should tag `FooConfig` so that it can be serialized/de-serialized as you plesae.
After creating the `Config` object, you can register it with the the generated `RegisterFooConfigType`. This allows you to create `NewFooFromType(fooType string)` and also `newFooConfigFromMap`.
In practice you will do something like: deserialize a JSON object into a `map[string]interface{}`, pass that map into `newFooConfigFromMap` which gives you your `FooConfig` and then call whatever you've implemented to get a `Foo` from a `FooConfig`
## The Future?
There's still a lot of boilerplate that needs to be generated to use this API. It would be nice to generate even more code, with more `go:generate` directives. For example, partial implementations of the XXXConfig objects could be generated, along with functions for deserializing the Config objects (or slices of them) from an io.Reader.

110
cmd/genconfig/main.go Normal file
View File

@ -0,0 +1,110 @@
// genconfig generates boilerplate configuration/registration code.
package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
)
var (
output = flag.String("o", "", "output file. Must be set")
)
const tmpl = `// DO NOT EDIT: This file was auto-generated by "go generate"
// To regenerate run:
// go install github.com/coreos/dex/cmd/genconfig
// go generate <<fully qualified package name>>
package __PKG__
import (
"encoding/json"
"errors"
"fmt"
)
type New__OBJ__ConfigFunc func() __OBJ__Config
var (
__obj__Types map[string]New__OBJ__ConfigFunc
)
func Register__OBJ__ConfigType(__obj__Type string, fn New__OBJ__ConfigFunc) {
if __obj__Types == nil {
__obj__Types = make(map[string]New__OBJ__ConfigFunc)
}
if _, ok := __obj__Types[__obj__Type]; ok {
panic(fmt.Sprintf("__obj__ config type %q already registered", __obj__Type))
}
__obj__Types[__obj__Type] = fn
}
func New__OBJ__ConfigFromType(__obj__Type string) (__OBJ__Config, error) {
fn, ok := __obj__Types[__obj__Type]
if !ok {
return nil, fmt.Errorf("unrecognized __obj__ config type %q", __obj__Type)
}
return fn(), nil
}
func new__OBJ__ConfigFromMap(m map[string]interface{}) (__OBJ__Config, error) {
ityp, ok := m["type"]
if !ok {
return nil, errors.New("__obj__ config type not set")
}
typ, ok := ityp.(string)
if !ok {
return nil, errors.New("__obj__ config type not string")
}
cfg, err := New__OBJ__ConfigFromType(typ)
if err != nil {
return nil, err
}
b, err := json.Marshal(m)
if err != nil {
return nil, err
}
if err = json.Unmarshal(b, cfg); err != nil {
return nil, err
}
return cfg, nil
}
`
// Usage is a replacement usage function for the flags package.
func Usage() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
fmt.Fprintf(os.Stderr, "\tgenconfig -o OutputFile PackageName ObjToConfigure \n")
fmt.Fprintf(os.Stderr, "Flags:\n")
flag.PrintDefaults()
}
func main() {
flag.Parse()
flag.Usage = Usage
args := flag.Args()
if *output == "" || len(args) != 2 {
flag.Usage()
os.Exit(2)
}
pkg := args[0]
objType := args[1]
objTypeLower := strings.ToLower(objType[0:1]) + objType[1:]
src := strings.Replace(tmpl, "__OBJ__", objType, -1)
src = strings.Replace(src, "__obj__", objTypeLower, -1)
src = strings.Replace(src, "__PKG__", pkg, -1)
err := ioutil.WriteFile(*output, []byte(src), 0644)
if err != nil {
log.Fatalf("writing output: %s", err)
}
}

96
cmd/gendoc/main.go Normal file
View File

@ -0,0 +1,96 @@
package main
import (
"fmt"
"io"
"os"
"github.com/coreos/dex/pkg/gendoc"
"github.com/spf13/cobra"
)
var cmd = &cobra.Command{
Use: "gendoc",
Short: "Generate documentation from REST specifications.",
Long: `A tool to generate documentation for dex's REST APIs.`,
RunE: gen,
}
var (
infile string
outfile string
readFlavor string
writeFlavor string
)
func init() {
cmd.PersistentFlags().StringVar(&infile, "f", "", "File to read from. If ommitted read from stdin.")
cmd.PersistentFlags().StringVar(&outfile, "o", "", "File to write to. If ommitted write to stdout.")
cmd.PersistentFlags().StringVar(&readFlavor, "r", "googleapi", "Flavor of REST spec to read. Currently only supports 'googleapi'.")
cmd.PersistentFlags().StringVar(&writeFlavor, "w", "markdown", "Flavor of documentation. Currently only supports 'markdown'.")
}
func main() {
if err := cmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
}
func gen(cmd *cobra.Command, args []string) error {
var (
in io.Reader
out io.Writer
decode func(io.Reader) (gendoc.Document, error)
encode func(gendoc.Document) ([]byte, error)
)
switch readFlavor {
case "googleapi":
decode = gendoc.ParseGoogleAPI
default:
return fmt.Errorf("unsupported read flavor %q", readFlavor)
}
switch writeFlavor {
case "markdown":
encode = gendoc.Document.MarshalMarkdown
default:
return fmt.Errorf("unsupported write flavor %q", writeFlavor)
}
if infile == "" {
in = os.Stdin
} else {
f, err := os.OpenFile(infile, os.O_RDONLY, 0644)
if err != nil {
return err
}
defer f.Close()
in = f
}
if outfile == "" {
out = os.Stdout
} else {
f, err := os.OpenFile(outfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer f.Close()
out = f
}
doc, err := decode(in)
if err != nil {
return fmt.Errorf("failed to decode input: %v", err)
}
data, err := encode(doc)
if err != nil {
return fmt.Errorf("failed to encode document: %v", err)
}
_, err = out.Write(data)
return err
}

View File

@ -1,35 +0,0 @@
issuer: http://127.0.0.1:5556/dex
storage:
type: sqlite3
config:
file: var/sqlite/dex.db
web:
http: 127.0.0.1:5556
telemetry:
http: 127.0.0.1:5558
grpc:
addr: 127.0.0.1:5557
staticClients:
- id: example-app
redirectURIs:
- 'http://127.0.0.1:5555/callback'
name: 'Example App'
secret: ZXhhbXBsZS1hcHAtc2VjcmV0
connectors:
- type: mockCallback
id: mock
name: Example
enablePasswordDB: true
staticPasswords:
- email: "admin@example.com"
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
username: "admin"
userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"

View File

@ -1,48 +0,0 @@
{{- /* NOTE: This configuration file is an example and exists only for development purposes. */ -}}
{{- /* To find more about gomplate formatting, please visit its documentation site - https://docs.gomplate.ca/ */ -}}
issuer: {{ getenv "DEX_ISSUER" "http://127.0.0.1:5556/dex" }}
storage:
type: sqlite3
config:
file: {{ getenv "DEX_STORAGE_SQLITE3_CONFIG_FILE" "/var/dex/dex.db" }}
web:
{{- if getenv "DEX_WEB_HTTPS" "" }}
https: {{ .Env.DEX_WEB_HTTPS }}
tlsKey: {{ getenv "DEX_WEB_TLS_KEY" | required "$DEX_WEB_TLS_KEY in case of web.https is enabled" }}
tlsCert: {{ getenv "DEX_WEB_TLS_CERT" | required "$DEX_WEB_TLS_CERT in case of web.https is enabled" }}
{{- end }}
http: {{ getenv "DEX_WEB_HTTP" "0.0.0.0:5556" }}
{{- if getenv "DEX_TELEMETRY_HTTP" }}
telemetry:
http: {{ .Env.DEX_TELEMETRY_HTTP }}
{{- end }}
expiry:
deviceRequests: {{ getenv "DEX_EXPIRY_DEVICE_REQUESTS" "5m" }}
signingKeys: {{ getenv "DEX_EXPIRY_SIGNING_KEYS" "6h" }}
idTokens: {{ getenv "DEX_EXPIRY_ID_TOKENS" "24h" }}
authRequests: {{ getenv "DEX_EXPIRY_AUTH_REQUESTS" "24h" }}
logger:
level: {{ getenv "DEX_LOG_LEVEL" "info" }}
format: {{ getenv "DEX_LOG_FORMAT" "text" }}
oauth2:
responseTypes: {{ getenv "DEX_OAUTH2_RESPONSE_TYPES" "[code]" }}
skipApprovalScreen: {{ getenv "DEX_OAUTH2_SKIP_APPROVAL_SCREEN" "false" }}
alwaysShowLoginScreen: {{ getenv "DEX_OAUTH2_ALWAYS_SHOW_LOGIN_SCREEN" "false" }}
{{- if getenv "DEX_OAUTH2_PASSWORD_CONNECTOR" "" }}
passwordConnector: {{ .Env.DEX_OAUTH2_PASSWORD_CONNECTOR }}
{{- end }}
enablePasswordDB: {{ getenv "DEX_ENABLE_PASSWORD_DB" "true" }}
connectors:
{{- if getenv "DEX_CONNECTORS_ENABLE_MOCK" }}
- type: mockCallback
id: mock
name: Example
{{- end }}

View File

@ -1,136 +0,0 @@
# The base path of Dex and the external name of the OpenID Connect service.
# This is the canonical URL that all clients MUST use to refer to Dex. If a
# path is provided, Dex's HTTP service will listen at a non-root URL.
issuer: http://127.0.0.1:5556/dex
# The storage configuration determines where Dex stores its state.
# Supported options include:
# - SQL flavors
# - key-value stores (eg. etcd)
# - Kubernetes Custom Resources
#
# See the documentation (https://dexidp.io/docs/storage/) for further information.
storage:
type: memory
# type: sqlite3
# config:
# file: /var/dex/dex.db
# type: mysql
# config:
# host: 127.0.0.1
# port: 3306
# database: dex
# user: mysql
# password: mysql
# ssl:
# mode: "false"
# type: postgres
# config:
# host: 127.0.0.1
# port: 5432
# database: dex
# user: postgres
# password: postgres
# ssl:
# mode: disable
# type: etcd
# config:
# endpoints:
# - http://127.0.0.1:2379
# namespace: dex/
# type: kubernetes
# config:
# kubeConfigFile: $HOME/.kube/config
# HTTP service configuration
web:
http: 127.0.0.1:5556
# Uncomment to enable HTTPS endpoint.
# https: 127.0.0.1:5554
# tlsCert: /etc/dex/tls.crt
# tlsKey: /etc/dex/tls.key
# Dex UI configuration
# frontend:
# issuer: dex
# logoURL: theme/logo.png
# dir: ""
# theme: light
# Telemetry configuration
# telemetry:
# http: 127.0.0.1:5558
# logger:
# level: "debug"
# format: "text" # can also be "json"
# gRPC API configuration
# Uncomment this block to enable the gRPC API.
# See the documentation (https://dexidp.io/docs/api/) for further information.
# grpc:
# addr: 127.0.0.1:5557
# tlsCert: examples/grpc-client/server.crt
# tlsKey: examples/grpc-client/server.key
# tlsClientCA: examples/grpc-client/ca.crt
# Expiration configuration for tokens, signing keys, etc.
# expiry:
# deviceRequests: "5m"
# signingKeys: "6h"
# idTokens: "24h"
# refreshTokens:
# disableRotation: false
# reuseInterval: "3s"
# validIfNotUsedFor: "2160h" # 90 days
# absoluteLifetime: "3960h" # 165 days
# OAuth2 configuration
# oauth2:
# # use ["code", "token", "id_token"] to enable implicit flow for web-only clients
# responseTypes: [ "code" ] # also allowed are "token" and "id_token"
#
# # By default, Dex will ask for approval to share data with application
# # (approval for sharing data from connected IdP to Dex is separate process on IdP)
# skipApprovalScreen: false
#
# # If only one authentication method is enabled, the default behavior is to
# # go directly to it. For connected IdPs, this redirects the browser away
# # from application to upstream provider such as the Google login page
# alwaysShowLoginScreen: false
#
# # Uncomment to use a specific connector for password grants
# passwordConnector: local
# Static clients registered in Dex by default.
#
# Alternatively, clients may be added through the gRPC API.
# staticClients:
# - id: example-app
# redirectURIs:
# - 'http://127.0.0.1:5555/callback'
# name: 'Example App'
# secret: ZXhhbXBsZS1hcHAtc2VjcmV0
# Connectors are used to authenticate users agains upstream identity providers.
#
# See the documentation (https://dexidp.io/docs/connectors/) for further information.
# connectors: []
# Enable the password database.
#
# It's a "virtual" connector (identity provider) that stores
# login credentials in Dex's store.
enablePasswordDB: true
# If this option isn't chosen users may be added through the gRPC API.
# A static list of passwords for the password connector.
#
# Alternatively, passwords my be added/updated through the gRPC API.
# staticPasswords: []

View File

@ -1,449 +0,0 @@
// Package atlassiancrowd provides authentication strategies using Atlassian Crowd.
package atlassiancrowd
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strings"
"time"
"github.com/dexidp/dex/connector"
"github.com/dexidp/dex/pkg/groups"
"github.com/dexidp/dex/pkg/log"
)
// Config holds configuration options for Atlassian Crowd connector.
// Crowd connectors require executing two queries, the first to find
// the user based on the username and password given to the connector.
// The second to use the user entry to search for groups.
//
// An example config:
//
// type: atlassian-crowd
// config:
// baseURL: https://crowd.example.com/context
// clientID: applogin
// clientSecret: appP4$$w0rd
// # users can be restricted by a list of groups
// groups:
// - admin
// # Prompt for username field
// usernamePrompt: Login
// preferredUsernameField: name
//
type Config struct {
BaseURL string `json:"baseURL"`
ClientID string `json:"clientID"`
ClientSecret string `json:"clientSecret"`
Groups []string `json:"groups"`
// PreferredUsernameField allows users to set the field to any of the
// following values: "key", "name" or "email".
// If unset, the preferred_username field will remain empty.
PreferredUsernameField string `json:"preferredUsernameField"`
// UsernamePrompt allows users to override the username attribute (displayed
// in the username/password prompt). If unset, the handler will use.
// "Username".
UsernamePrompt string `json:"usernamePrompt"`
}
type crowdUser struct {
Key string
Name string
Active bool
Email string
}
type crowdGroups struct {
Groups []struct {
Name string
} `json:"groups"`
}
type crowdAuthentication struct {
Token string
User struct {
Name string
} `json:"user"`
CreatedDate uint64 `json:"created-date"`
ExpiryDate uint64 `json:"expiry-date"`
}
type crowdAuthenticationError struct {
Reason string
Message string
}
// Open returns a strategy for logging in through Atlassian Crowd
func (c *Config) Open(_ string, logger log.Logger) (connector.Connector, error) {
if c.BaseURL == "" {
return nil, fmt.Errorf("crowd: no baseURL provided for crowd connector")
}
return &crowdConnector{Config: *c, logger: logger}, nil
}
type crowdConnector struct {
Config
logger log.Logger
}
var (
_ connector.PasswordConnector = (*crowdConnector)(nil)
_ connector.RefreshConnector = (*crowdConnector)(nil)
)
type refreshData struct {
Username string `json:"username"`
}
func (c *crowdConnector) Login(ctx context.Context, s connector.Scopes, username, password string) (ident connector.Identity, validPass bool, err error) {
// make this check to avoid empty passwords.
if password == "" {
return connector.Identity{}, false, nil
}
// We want to return a different error if the user's password is incorrect vs
// if there was an error.
var incorrectPass bool
var user crowdUser
client := c.crowdAPIClient()
if incorrectPass, err = c.authenticateWithPassword(ctx, client, username, password); err != nil {
return connector.Identity{}, false, err
}
if incorrectPass {
return connector.Identity{}, false, nil
}
if user, err = c.user(ctx, client, username); err != nil {
return connector.Identity{}, false, err
}
ident = c.identityFromCrowdUser(user)
if s.Groups {
userGroups, err := c.getGroups(ctx, client, s.Groups, ident.Username)
if err != nil {
return connector.Identity{}, false, fmt.Errorf("crowd: failed to query groups: %v", err)
}
ident.Groups = userGroups
}
if s.OfflineAccess {
refresh := refreshData{Username: username}
// Encode entry for following up requests such as the groups query and refresh attempts.
if ident.ConnectorData, err = json.Marshal(refresh); err != nil {
return connector.Identity{}, false, fmt.Errorf("crowd: marshal refresh data: %v", err)
}
}
return ident, true, nil
}
func (c *crowdConnector) Refresh(ctx context.Context, s connector.Scopes, ident connector.Identity) (connector.Identity, error) {
var data refreshData
if err := json.Unmarshal(ident.ConnectorData, &data); err != nil {
return ident, fmt.Errorf("crowd: failed to unmarshal internal data: %v", err)
}
var user crowdUser
client := c.crowdAPIClient()
user, err := c.user(ctx, client, data.Username)
if err != nil {
return ident, fmt.Errorf("crowd: get user %q: %v", data.Username, err)
}
newIdent := c.identityFromCrowdUser(user)
newIdent.ConnectorData = ident.ConnectorData
// If user exists, authenticate it to prolong sso session.
err = c.authenticateUser(ctx, client, data.Username)
if err != nil {
return ident, fmt.Errorf("crowd: authenticate user: %v", err)
}
if s.Groups {
userGroups, err := c.getGroups(ctx, client, s.Groups, newIdent.Username)
if err != nil {
return connector.Identity{}, fmt.Errorf("crowd: failed to query groups: %v", err)
}
newIdent.Groups = userGroups
}
return newIdent, nil
}
func (c *crowdConnector) Prompt() string {
return c.UsernamePrompt
}
func (c *crowdConnector) crowdAPIClient() *http.Client {
return &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}
}
// authenticateWithPassword creates a new session for user and validates a password with Crowd API
func (c *crowdConnector) authenticateWithPassword(ctx context.Context, client *http.Client, username string, password string) (invalidPass bool, err error) {
req, err := c.crowdUserManagementRequest(ctx,
"POST",
"/session",
struct {
Username string `json:"username"`
Password string `json:"password"`
}{Username: username, Password: password},
)
if err != nil {
return false, fmt.Errorf("crowd: new auth pass api request %v", err)
}
resp, err := client.Do(req)
if err != nil {
return false, fmt.Errorf("crowd: api request %v", err)
}
defer resp.Body.Close()
body, err := c.validateCrowdResponse(resp)
if err != nil {
return false, err
}
if resp.StatusCode != http.StatusCreated {
var authError crowdAuthenticationError
if err := json.Unmarshal(body, &authError); err != nil {
return false, fmt.Errorf("unmarshal auth pass response: %d %v %q", resp.StatusCode, err, string(body))
}
if authError.Reason == "INVALID_USER_AUTHENTICATION" {
return true, nil
}
return false, fmt.Errorf("%s: %s", resp.Status, authError.Message)
}
var authResponse crowdAuthentication
if err := json.Unmarshal(body, &authResponse); err != nil {
return false, fmt.Errorf("decode auth response: %v", err)
}
return false, nil
}
// authenticateUser creates a new session for user without password validations with Crowd API
func (c *crowdConnector) authenticateUser(ctx context.Context, client *http.Client, username string) error {
req, err := c.crowdUserManagementRequest(ctx,
"POST",
"/session?validate-password=false",
struct {
Username string `json:"username"`
}{Username: username},
)
if err != nil {
return fmt.Errorf("crowd: new auth api request %v", err)
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("crowd: api request %v", err)
}
defer resp.Body.Close()
body, err := c.validateCrowdResponse(resp)
if err != nil {
return err
}
if resp.StatusCode != http.StatusCreated {
return fmt.Errorf("%s: %s", resp.Status, body)
}
var authResponse crowdAuthentication
if err := json.Unmarshal(body, &authResponse); err != nil {
return fmt.Errorf("decode auth response: %v", err)
}
return nil
}
// user retrieves user info from Crowd API
func (c *crowdConnector) user(ctx context.Context, client *http.Client, username string) (crowdUser, error) {
var user crowdUser
req, err := c.crowdUserManagementRequest(ctx,
"GET",
fmt.Sprintf("/user?username=%s", username),
nil,
)
if err != nil {
return user, fmt.Errorf("crowd: new user api request %v", err)
}
resp, err := client.Do(req)
if err != nil {
return user, fmt.Errorf("crowd: api request %v", err)
}
defer resp.Body.Close()
body, err := c.validateCrowdResponse(resp)
if err != nil {
return user, err
}
if resp.StatusCode != http.StatusOK {
return user, fmt.Errorf("%s: %s", resp.Status, body)
}
if err := json.Unmarshal(body, &user); err != nil {
return user, fmt.Errorf("failed to decode response: %v", err)
}
return user, nil
}
// groups retrieves groups from Crowd API
func (c *crowdConnector) groups(ctx context.Context, client *http.Client, username string) (userGroups []string, err error) {
var crowdGroups crowdGroups
req, err := c.crowdUserManagementRequest(ctx,
"GET",
fmt.Sprintf("/user/group/nested?username=%s", username),
nil,
)
if err != nil {
return nil, fmt.Errorf("crowd: new groups api request %v", err)
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("crowd: api request %v", err)
}
defer resp.Body.Close()
body, err := c.validateCrowdResponse(resp)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s: %s", resp.Status, body)
}
if err := json.Unmarshal(body, &crowdGroups); err != nil {
return nil, fmt.Errorf("failed to decode response: %v", err)
}
for _, group := range crowdGroups.Groups {
userGroups = append(userGroups, group.Name)
}
return userGroups, nil
}
// identityFromCrowdUser converts crowdUser to Identity
func (c *crowdConnector) identityFromCrowdUser(user crowdUser) connector.Identity {
identity := connector.Identity{
Username: user.Name,
UserID: user.Key,
Email: user.Email,
EmailVerified: true,
}
switch c.PreferredUsernameField {
case "key":
identity.PreferredUsername = user.Key
case "name":
identity.PreferredUsername = user.Name
case "email":
identity.PreferredUsername = user.Email
default:
if c.PreferredUsernameField != "" {
c.logger.Warnf("preferred_username left empty. Invalid crowd field mapped to preferred_username: %s", c.PreferredUsernameField)
}
}
return identity
}
// getGroups retrieves a list of user's groups and filters it
func (c *crowdConnector) getGroups(ctx context.Context, client *http.Client, groupScope bool, userLogin string) ([]string, error) {
crowdGroups, err := c.groups(ctx, client, userLogin)
if err != nil {
return nil, err
}
if len(c.Groups) > 0 {
filteredGroups := groups.Filter(crowdGroups, c.Groups)
if len(filteredGroups) == 0 {
return nil, fmt.Errorf("crowd: user %q is not in any of the required groups", userLogin)
}
return filteredGroups, nil
} else if groupScope {
return crowdGroups, nil
}
return nil, nil
}
// crowdUserManagementRequest create a http.Request with basic auth, json payload and Accept header
func (c *crowdConnector) crowdUserManagementRequest(ctx context.Context, method string, apiURL string, jsonPayload interface{}) (*http.Request, error) {
var body io.Reader
if jsonPayload != nil {
jsonData, err := json.Marshal(jsonPayload)
if err != nil {
return nil, fmt.Errorf("crowd: marshal API json payload: %v", err)
}
body = bytes.NewReader(jsonData)
}
req, err := http.NewRequest(method, fmt.Sprintf("%s/rest/usermanagement/1%s", c.BaseURL, apiURL), body)
if err != nil {
return nil, fmt.Errorf("new API req: %v", err)
}
req = req.WithContext(ctx)
// Crowd API requires a basic auth
req.SetBasicAuth(c.ClientID, c.ClientSecret)
req.Header.Set("Accept", "application/json")
if jsonPayload != nil {
req.Header.Set("Content-type", "application/json")
}
return req, nil
}
// validateCrowdResponse validates unique not JSON responses from API
func (c *crowdConnector) validateCrowdResponse(resp *http.Response) ([]byte, error) {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("crowd: read user body: %v", err)
}
if resp.StatusCode == http.StatusForbidden && strings.Contains(string(body), "The server understood the request but refuses to authorize it.") {
c.logger.Debugf("crowd response validation failed: %s", string(body))
return nil, fmt.Errorf("dex is forbidden from making requests to the Atlassian Crowd application by URL %q", c.BaseURL)
}
if resp.StatusCode == http.StatusUnauthorized && string(body) == "Application failed to authenticate" {
c.logger.Debugf("crowd response validation failed: %s", string(body))
return nil, fmt.Errorf("dex failed to authenticate Crowd Application with ID %q", c.ClientID)
}
return body, nil
}

View File

@ -1,189 +0,0 @@
// Package atlassiancrowd provides authentication strategies using Atlassian Crowd.
package atlassiancrowd
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"github.com/sirupsen/logrus"
)
func TestUserGroups(t *testing.T) {
s := newTestServer(map[string]TestServerResponse{
"/rest/usermanagement/1/user/group/nested?username=testuser": {
Body: crowdGroups{Groups: []struct{ Name string }{{Name: "group1"}, {Name: "group2"}}},
Code: 200,
},
})
defer s.Close()
c := newTestCrowdConnector(s.URL)
groups, err := c.getGroups(context.Background(), newClient(), true, "testuser")
expectNil(t, err)
expectEquals(t, groups, []string{"group1", "group2"})
}
func TestUserGroupsWithFiltering(t *testing.T) {
s := newTestServer(map[string]TestServerResponse{
"/rest/usermanagement/1/user/group/nested?username=testuser": {
Body: crowdGroups{Groups: []struct{ Name string }{{Name: "group1"}, {Name: "group2"}}},
Code: 200,
},
})
defer s.Close()
c := newTestCrowdConnector(s.URL)
c.Groups = []string{"group1"}
groups, err := c.getGroups(context.Background(), newClient(), true, "testuser")
expectNil(t, err)
expectEquals(t, groups, []string{"group1"})
}
func TestUserLoginFlow(t *testing.T) {
s := newTestServer(map[string]TestServerResponse{
"/rest/usermanagement/1/session?validate-password=false": {
Body: crowdAuthentication{},
Code: 201,
},
"/rest/usermanagement/1/user?username=testuser": {
Body: crowdUser{Active: true, Name: "testuser", Email: "testuser@example.com"},
Code: 200,
},
"/rest/usermanagement/1/user?username=testuser2": {
Body: `<html>The server understood the request but refuses to authorize it.</html>`,
Code: 403,
},
})
defer s.Close()
c := newTestCrowdConnector(s.URL)
user, err := c.user(context.Background(), newClient(), "testuser")
expectNil(t, err)
expectEquals(t, user.Name, "testuser")
expectEquals(t, user.Email, "testuser@example.com")
err = c.authenticateUser(context.Background(), newClient(), "testuser")
expectNil(t, err)
_, err = c.user(context.Background(), newClient(), "testuser2")
expectEquals(t, err, fmt.Errorf("dex is forbidden from making requests to the Atlassian Crowd application by URL %q", s.URL))
}
func TestUserPassword(t *testing.T) {
s := newTestServer(map[string]TestServerResponse{
"/rest/usermanagement/1/session": {
Body: crowdAuthenticationError{Reason: "INVALID_USER_AUTHENTICATION", Message: "test"},
Code: 401,
},
"/rest/usermanagement/1/session?validate-password=false": {
Body: crowdAuthentication{},
Code: 201,
},
})
defer s.Close()
c := newTestCrowdConnector(s.URL)
invalidPassword, err := c.authenticateWithPassword(context.Background(), newClient(), "testuser", "testpassword")
expectNil(t, err)
expectEquals(t, invalidPassword, true)
err = c.authenticateUser(context.Background(), newClient(), "testuser")
expectNil(t, err)
}
func TestIdentityFromCrowdUser(t *testing.T) {
user := crowdUser{
Key: "12345",
Name: "testuser",
Active: true,
Email: "testuser@example.com",
}
c := newTestCrowdConnector("/")
// Sanity checks
expectEquals(t, user.Name, "testuser")
expectEquals(t, user.Email, "testuser@example.com")
// Test unconfigured behaviour
i := c.identityFromCrowdUser(user)
expectEquals(t, i.UserID, "12345")
expectEquals(t, i.Username, "testuser")
expectEquals(t, i.Email, "testuser@example.com")
expectEquals(t, i.EmailVerified, true)
// Test for various PreferredUsernameField settings
// unset
expectEquals(t, i.PreferredUsername, "")
c.Config.PreferredUsernameField = "key"
i = c.identityFromCrowdUser(user)
expectEquals(t, i.PreferredUsername, "12345")
c.Config.PreferredUsernameField = "name"
i = c.identityFromCrowdUser(user)
expectEquals(t, i.PreferredUsername, "testuser")
c.Config.PreferredUsernameField = "email"
i = c.identityFromCrowdUser(user)
expectEquals(t, i.PreferredUsername, "testuser@example.com")
c.Config.PreferredUsernameField = "invalidstring"
i = c.identityFromCrowdUser(user)
expectEquals(t, i.PreferredUsername, "")
}
type TestServerResponse struct {
Body interface{}
Code int
}
func newTestCrowdConnector(baseURL string) crowdConnector {
connector := crowdConnector{}
connector.BaseURL = baseURL
connector.logger = &logrus.Logger{
Out: io.Discard,
Level: logrus.DebugLevel,
Formatter: &logrus.TextFormatter{DisableColors: true},
}
return connector
}
func newTestServer(responses map[string]TestServerResponse) *httptest.Server {
s := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
response := responses[r.RequestURI]
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(response.Code)
json.NewEncoder(w).Encode(response.Body)
}))
return s
}
func newClient() *http.Client {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
return &http.Client{Transport: tr}
}
func expectNil(t *testing.T, a interface{}) {
if a != nil {
t.Errorf("Expected %+v to equal nil", a)
}
}
func expectEquals(t *testing.T, a interface{}, b interface{}) {
if !reflect.DeepEqual(a, b) {
t.Errorf("Expected %+v to equal %+v", a, b)
}
}

View File

@ -1,80 +0,0 @@
// Package authproxy implements a connector which relies on external
// authentication (e.g. mod_auth in Apache2) and returns an identity with the
// HTTP header X-Remote-User as verified email.
package authproxy
import (
"fmt"
"net/http"
"net/url"
"github.com/dexidp/dex/connector"
"github.com/dexidp/dex/pkg/log"
)
// Config holds the configuration parameters for a connector which returns an
// identity with the HTTP header X-Remote-User as verified email,
// X-Remote-Group and configured staticGroups as user's group.
// Headers retrieved to fetch user's email and group can be configured
// with userHeader and groupHeader.
type Config struct {
UserHeader string `json:"userHeader"`
GroupHeader string `json:"groupHeader"`
Groups []string `json:"staticGroups"`
}
// Open returns an authentication strategy which requires no user interaction.
func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
userHeader := c.UserHeader
if userHeader == "" {
userHeader = "X-Remote-User"
}
groupHeader := c.GroupHeader
if groupHeader == "" {
groupHeader = "X-Remote-Group"
}
return &callback{userHeader: userHeader, groupHeader: groupHeader, logger: logger, pathSuffix: "/" + id, groups: c.Groups}, nil
}
// Callback is a connector which returns an identity with the HTTP header
// X-Remote-User as verified email.
type callback struct {
userHeader string
groupHeader string
groups []string
logger log.Logger
pathSuffix string
}
// LoginURL returns the URL to redirect the user to login with.
func (m *callback) LoginURL(s connector.Scopes, callbackURL, state string) (string, error) {
u, err := url.Parse(callbackURL)
if err != nil {
return "", fmt.Errorf("failed to parse callbackURL %q: %v", callbackURL, err)
}
u.Path += m.pathSuffix
v := u.Query()
v.Set("state", state)
u.RawQuery = v.Encode()
return u.String(), nil
}
// HandleCallback parses the request and returns the user's identity
func (m *callback) HandleCallback(s connector.Scopes, r *http.Request) (connector.Identity, error) {
remoteUser := r.Header.Get(m.userHeader)
if remoteUser == "" {
return connector.Identity{}, fmt.Errorf("required HTTP header %s is not set", m.userHeader)
}
groups := m.groups
headerGroup := r.Header.Get(m.groupHeader)
if headerGroup != "" {
groups = append(groups, headerGroup)
}
return connector.Identity{
UserID: remoteUser, // TODO: figure out if this is a bad ID value.
Email: remoteUser,
EmailVerified: true,
Groups: groups,
}, nil
}

View File

@ -1,468 +0,0 @@
// Package bitbucketcloud provides authentication strategies using Bitbucket Cloud.
package bitbucketcloud
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"sync"
"time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/bitbucket"
"github.com/dexidp/dex/connector"
"github.com/dexidp/dex/pkg/groups"
"github.com/dexidp/dex/pkg/log"
)
const (
apiURL = "https://api.bitbucket.org/2.0"
// Switch to API v2.0 when the Atlassian platform services are fully available in Bitbucket
legacyAPIURL = "https://api.bitbucket.org/1.0"
// Bitbucket requires this scope to access '/user' API endpoints.
scopeAccount = "account"
// Bitbucket requires this scope to access '/user/emails' API endpoints.
scopeEmail = "email"
// Bitbucket requires this scope to access '/teams' API endpoints
// which are used when a client includes the 'groups' scope.
scopeTeams = "team"
)
// Config holds configuration options for Bitbucket logins.
type Config struct {
ClientID string `json:"clientID"`
ClientSecret string `json:"clientSecret"`
RedirectURI string `json:"redirectURI"`
Teams []string `json:"teams"`
IncludeTeamGroups bool `json:"includeTeamGroups,omitempty"`
}
// Open returns a strategy for logging in through Bitbucket.
func (c *Config) Open(_ string, logger log.Logger) (connector.Connector, error) {
b := bitbucketConnector{
redirectURI: c.RedirectURI,
teams: c.Teams,
clientID: c.ClientID,
clientSecret: c.ClientSecret,
includeTeamGroups: c.IncludeTeamGroups,
apiURL: apiURL,
legacyAPIURL: legacyAPIURL,
logger: logger,
}
return &b, nil
}
type connectorData struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
Expiry time.Time `json:"expiry"`
}
var (
_ connector.CallbackConnector = (*bitbucketConnector)(nil)
_ connector.RefreshConnector = (*bitbucketConnector)(nil)
)
type bitbucketConnector struct {
redirectURI string
teams []string
clientID string
clientSecret string
logger log.Logger
apiURL string
legacyAPIURL string
// the following are used only for tests
hostName string
httpClient *http.Client
includeTeamGroups bool
}
// groupsRequired returns whether dex requires Bitbucket's 'team' scope.
func (b *bitbucketConnector) groupsRequired(groupScope bool) bool {
return len(b.teams) > 0 || groupScope
}
func (b *bitbucketConnector) oauth2Config(scopes connector.Scopes) *oauth2.Config {
bitbucketScopes := []string{scopeAccount, scopeEmail}
if b.groupsRequired(scopes.Groups) {
bitbucketScopes = append(bitbucketScopes, scopeTeams)
}
endpoint := bitbucket.Endpoint
if b.hostName != "" {
endpoint = oauth2.Endpoint{
AuthURL: "https://" + b.hostName + "/site/oauth2/authorize",
TokenURL: "https://" + b.hostName + "/site/oauth2/access_token",
}
}
return &oauth2.Config{
ClientID: b.clientID,
ClientSecret: b.clientSecret,
Endpoint: endpoint,
Scopes: bitbucketScopes,
}
}
func (b *bitbucketConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, error) {
if b.redirectURI != callbackURL {
return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, b.redirectURI)
}
return b.oauth2Config(scopes).AuthCodeURL(state), nil
}
type oauth2Error struct {
error string
errorDescription string
}
func (e *oauth2Error) Error() string {
if e.errorDescription == "" {
return e.error
}
return e.error + ": " + e.errorDescription
}
func (b *bitbucketConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) {
q := r.URL.Query()
if errType := q.Get("error"); errType != "" {
return identity, &oauth2Error{errType, q.Get("error_description")}
}
oauth2Config := b.oauth2Config(s)
ctx := r.Context()
if b.httpClient != nil {
ctx = context.WithValue(r.Context(), oauth2.HTTPClient, b.httpClient)
}
token, err := oauth2Config.Exchange(ctx, q.Get("code"))
if err != nil {
return identity, fmt.Errorf("bitbucket: failed to get token: %v", err)
}
client := oauth2Config.Client(ctx, token)
user, err := b.user(ctx, client)
if err != nil {
return identity, fmt.Errorf("bitbucket: get user: %v", err)
}
identity = connector.Identity{
UserID: user.UUID,
Username: user.Username,
Email: user.Email,
EmailVerified: true,
}
if b.groupsRequired(s.Groups) {
groups, err := b.getGroups(ctx, client, s.Groups, user.Username)
if err != nil {
return identity, err
}
identity.Groups = groups
}
if s.OfflineAccess {
data := connectorData{
AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
Expiry: token.Expiry,
}
connData, err := json.Marshal(data)
if err != nil {
return identity, fmt.Errorf("bitbucket: marshal connector data: %v", err)
}
identity.ConnectorData = connData
}
return identity, nil
}
// Refreshing tokens
// https://github.com/golang/oauth2/issues/84#issuecomment-332860871
type tokenNotifyFunc func(*oauth2.Token) error
// notifyRefreshTokenSource is essentially `oauth2.ReuseTokenSource` with `TokenNotifyFunc` added.
type notifyRefreshTokenSource struct {
new oauth2.TokenSource
mu sync.Mutex // guards t
t *oauth2.Token
f tokenNotifyFunc // called when token refreshed so new refresh token can be persisted
}
// Token returns the current token if it's still valid, else will
// refresh the current token (using r.Context for HTTP client
// information) and return the new one.
func (s *notifyRefreshTokenSource) Token() (*oauth2.Token, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.t.Valid() {
return s.t, nil
}
t, err := s.new.Token()
if err != nil {
return nil, err
}
s.t = t
return t, s.f(t)
}
func (b *bitbucketConnector) Refresh(ctx context.Context, s connector.Scopes, identity connector.Identity) (connector.Identity, error) {
if len(identity.ConnectorData) == 0 {
return identity, errors.New("bitbucket: no upstream access token found")
}
var data connectorData
if err := json.Unmarshal(identity.ConnectorData, &data); err != nil {
return identity, fmt.Errorf("bitbucket: unmarshal access token: %v", err)
}
tok := &oauth2.Token{
AccessToken: data.AccessToken,
RefreshToken: data.RefreshToken,
Expiry: data.Expiry,
}
client := oauth2.NewClient(ctx, &notifyRefreshTokenSource{
new: b.oauth2Config(s).TokenSource(ctx, tok),
t: tok,
f: func(tok *oauth2.Token) error {
data := connectorData{
AccessToken: tok.AccessToken,
RefreshToken: tok.RefreshToken,
Expiry: tok.Expiry,
}
connData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("bitbucket: marshal connector data: %v", err)
}
identity.ConnectorData = connData
return nil
},
})
user, err := b.user(ctx, client)
if err != nil {
return identity, fmt.Errorf("bitbucket: get user: %v", err)
}
identity.Username = user.Username
identity.Email = user.Email
if b.groupsRequired(s.Groups) {
groups, err := b.getGroups(ctx, client, s.Groups, user.Username)
if err != nil {
return identity, err
}
identity.Groups = groups
}
return identity, nil
}
// Bitbucket pagination wrapper
type pagedResponse struct {
Size int `json:"size"`
Page int `json:"page"`
PageLen int `json:"pagelen"`
Next *string `json:"next"`
Previous *string `json:"previous"`
}
// user holds Bitbucket user information (relevant to dex) as defined by
// https://developer.atlassian.com/bitbucket/api/2/reference/resource/user
type user struct {
Username string `json:"username"`
UUID string `json:"uuid"`
Email string `json:"email"`
}
// user queries the Bitbucket API for profile information using the provided client.
//
// The HTTP client is expected to be constructed by the golang.org/x/oauth2 package,
// which inserts a bearer token as part of the request.
func (b *bitbucketConnector) user(ctx context.Context, client *http.Client) (user, error) {
// https://developer.atlassian.com/bitbucket/api/2/reference/resource/user
var (
u user
err error
)
if err = get(ctx, client, b.apiURL+"/user", &u); err != nil {
return user{}, err
}
if u.Email, err = b.userEmail(ctx, client); err != nil {
return user{}, err
}
return u, nil
}
// userEmail holds Bitbucket user email information as defined by
// https://developer.atlassian.com/bitbucket/api/2/reference/resource/user/emails
type userEmail struct {
IsPrimary bool `json:"is_primary"`
IsConfirmed bool `json:"is_confirmed"`
Email string `json:"email"`
}
type userEmailResponse struct {
pagedResponse
Values []userEmail
}
// userEmail returns the users primary, confirmed email
//
// The HTTP client is expected to be constructed by the golang.org/x/oauth2 package,
// which inserts a bearer token as part of the request.
func (b *bitbucketConnector) userEmail(ctx context.Context, client *http.Client) (string, error) {
apiURL := b.apiURL + "/user/emails"
for {
// https://developer.atlassian.com/bitbucket/api/2/reference/resource/user/emails
var response userEmailResponse
if err := get(ctx, client, apiURL, &response); err != nil {
return "", err
}
for _, email := range response.Values {
if email.IsConfirmed && email.IsPrimary {
return email.Email, nil
}
}
if response.Next == nil {
break
}
}
return "", errors.New("bitbucket: user has no confirmed, primary email")
}
// getGroups retrieves Bitbucket teams a user is in, if any.
func (b *bitbucketConnector) getGroups(ctx context.Context, client *http.Client, groupScope bool, userLogin string) ([]string, error) {
bitbucketTeams, err := b.userWorkspaces(ctx, client)
if err != nil {
return nil, err
}
if len(b.teams) > 0 {
filteredTeams := groups.Filter(bitbucketTeams, b.teams)
if len(filteredTeams) == 0 {
return nil, fmt.Errorf("bitbucket: user %q is not in any of the required teams", userLogin)
}
return filteredTeams, nil
} else if groupScope {
return bitbucketTeams, nil
}
return nil, nil
}
type workspaceSlug struct {
Slug string `json:"slug"`
}
type workspace struct {
Workspace workspaceSlug `json:"workspace"`
}
type userWorkspacesResponse struct {
pagedResponse
Values []workspace `json:"values"`
}
func (b *bitbucketConnector) userWorkspaces(ctx context.Context, client *http.Client) ([]string, error) {
var teams []string
apiURL := b.apiURL + "/user/permissions/workspaces"
for {
// https://developer.atlassian.com/cloud/bitbucket/rest/api-group-workspaces/#api-workspaces-get
var response userWorkspacesResponse
if err := get(ctx, client, apiURL, &response); err != nil {
return nil, fmt.Errorf("bitbucket: get user teams: %v", err)
}
for _, value := range response.Values {
teams = append(teams, value.Workspace.Slug)
}
if response.Next == nil {
break
}
}
if b.includeTeamGroups {
for _, team := range teams {
teamGroups, err := b.userTeamGroups(ctx, client, team)
if err != nil {
return nil, fmt.Errorf("bitbucket: %v", err)
}
teams = append(teams, teamGroups...)
}
}
return teams, nil
}
type group struct {
Slug string `json:"slug"`
}
func (b *bitbucketConnector) userTeamGroups(ctx context.Context, client *http.Client, teamName string) ([]string, error) {
apiURL := b.legacyAPIURL + "/groups/" + teamName
var response []group
if err := get(ctx, client, apiURL, &response); err != nil {
return nil, fmt.Errorf("get user team %q groups: %v", teamName, err)
}
teamGroups := make([]string, 0, len(response))
for _, group := range response {
teamGroups = append(teamGroups, teamName+"/"+group.Slug)
}
return teamGroups, nil
}
// get creates a "GET `apiURL`" request with context, sends the request using
// the client, and decodes the resulting response body into v.
// Any errors encountered when building requests, sending requests, and
// reading and decoding response data are returned.
func get(ctx context.Context, client *http.Client, apiURL string, v interface{}) error {
req, err := http.NewRequest("GET", apiURL, nil)
if err != nil {
return fmt.Errorf("bitbucket: new req: %v", err)
}
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("bitbucket: get URL %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("bitbucket: read body: %s: %v", resp.Status, err)
}
return fmt.Errorf("%s: %s", resp.Status, body)
}
if err := json.NewDecoder(resp.Body).Decode(v); err != nil {
return fmt.Errorf("bitbucket: failed to decode response: %v", err)
}
return nil
}

View File

@ -1,137 +0,0 @@
package bitbucketcloud
import (
"context"
"crypto/tls"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"testing"
"github.com/dexidp/dex/connector"
)
func TestUserGroups(t *testing.T) {
teamsResponse := userWorkspacesResponse{
pagedResponse: pagedResponse{
Size: 3,
Page: 1,
PageLen: 10,
},
Values: []workspace{
{Workspace: workspaceSlug{Slug: "team-1"}},
{Workspace: workspaceSlug{Slug: "team-2"}},
{Workspace: workspaceSlug{Slug: "team-3"}},
},
}
s := newTestServer(map[string]interface{}{
"/user/permissions/workspaces": teamsResponse,
"/groups/team-1": []group{{Slug: "administrators"}, {Slug: "members"}},
"/groups/team-2": []group{{Slug: "everyone"}},
"/groups/team-3": []group{},
})
connector := bitbucketConnector{apiURL: s.URL, legacyAPIURL: s.URL}
groups, err := connector.userWorkspaces(context.Background(), newClient())
expectNil(t, err)
expectEquals(t, groups, []string{
"team-1",
"team-2",
"team-3",
})
connector.includeTeamGroups = true
groups, err = connector.userWorkspaces(context.Background(), newClient())
expectNil(t, err)
expectEquals(t, groups, []string{
"team-1",
"team-2",
"team-3",
"team-1/administrators",
"team-1/members",
"team-2/everyone",
})
s.Close()
}
func TestUserWithoutTeams(t *testing.T) {
s := newTestServer(map[string]interface{}{
"/user/permissions/workspaces": userWorkspacesResponse{},
})
connector := bitbucketConnector{apiURL: s.URL}
groups, err := connector.userWorkspaces(context.Background(), newClient())
expectNil(t, err)
expectEquals(t, len(groups), 0)
s.Close()
}
func TestUsernameIncludedInFederatedIdentity(t *testing.T) {
s := newTestServer(map[string]interface{}{
"/user": user{Username: "some-login"},
"/user/emails": userEmailResponse{
pagedResponse: pagedResponse{
Size: 1,
Page: 1,
PageLen: 10,
},
Values: []userEmail{{
Email: "some@email.com",
IsConfirmed: true,
IsPrimary: true,
}},
},
"/site/oauth2/access_token": map[string]interface{}{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9",
"expires_in": "30",
},
})
hostURL, err := url.Parse(s.URL)
expectNil(t, err)
req, err := http.NewRequest("GET", hostURL.String(), nil)
expectNil(t, err)
bitbucketConnector := bitbucketConnector{apiURL: s.URL, hostName: hostURL.Host, httpClient: newClient()}
identity, err := bitbucketConnector.HandleCallback(connector.Scopes{}, req)
expectNil(t, err)
expectEquals(t, identity.Username, "some-login")
s.Close()
}
func newTestServer(responses map[string]interface{}) *httptest.Server {
return httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(responses[r.URL.String()])
}))
}
func newClient() *http.Client {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
return &http.Client{Transport: tr}
}
func expectNil(t *testing.T, a interface{}) {
if a != nil {
t.Fatalf("Expected %+v to equal nil", a)
}
}
func expectEquals(t *testing.T, a interface{}, b interface{}) {
if !reflect.DeepEqual(a, b) {
t.Fatalf("Expected %+v to equal %+v", a, b)
}
}

62
connector/config.go Normal file
View File

@ -0,0 +1,62 @@
// DO NOT EDIT: This file was auto-generated by "go generate"
// To regenerate run:
// go install github.com/coreos/dex/cmd/genconfig
// go generate <<fully qualified package name>>
package connector
import (
"encoding/json"
"errors"
"fmt"
)
type NewConnectorConfigFunc func() ConnectorConfig
var (
connectorTypes map[string]NewConnectorConfigFunc
)
func RegisterConnectorConfigType(connectorType string, fn NewConnectorConfigFunc) {
if connectorTypes == nil {
connectorTypes = make(map[string]NewConnectorConfigFunc)
}
if _, ok := connectorTypes[connectorType]; ok {
panic(fmt.Sprintf("connector config type %q already registered", connectorType))
}
connectorTypes[connectorType] = fn
}
func NewConnectorConfigFromType(connectorType string) (ConnectorConfig, error) {
fn, ok := connectorTypes[connectorType]
if !ok {
return nil, fmt.Errorf("unrecognized connector config type %q", connectorType)
}
return fn(), nil
}
func newConnectorConfigFromMap(m map[string]interface{}) (ConnectorConfig, error) {
ityp, ok := m["type"]
if !ok {
return nil, errors.New("connector config type not set")
}
typ, ok := ityp.(string)
if !ok {
return nil, errors.New("connector config type not string")
}
cfg, err := NewConnectorConfigFromType(typ)
if err != nil {
return nil, err
}
b, err := json.Marshal(m)
if err != nil {
return nil, err
}
if err = json.Unmarshal(b, cfg); err != nil {
return nil, err
}
return cfg, nil
}

22
connector/config_repo.go Normal file
View File

@ -0,0 +1,22 @@
package connector
import (
"encoding/json"
"io"
)
func ReadConfigs(r io.Reader) ([]ConnectorConfig, error) {
var ms []map[string]interface{}
if err := json.NewDecoder(r).Decode(&ms); err != nil {
return nil, err
}
cfgs := make([]ConnectorConfig, len(ms))
for i, m := range ms {
cfg, err := newConnectorConfigFromMap(m)
if err != nil {
return nil, err
}
cfgs[i] = cfg
}
return cfgs, nil
}

View File

@ -0,0 +1,117 @@
package connector
import (
"reflect"
"strings"
"testing"
"github.com/kylelemons/godebug/pretty"
"github.com/coreos/dex/user"
)
func TestNewConnectorConfigFromType(t *testing.T) {
tests := []struct {
typ string
want interface{}
}{
{
typ: LocalConnectorType,
want: &LocalConnectorConfig{},
},
{
typ: OIDCConnectorType,
want: &OIDCConnectorConfig{},
},
}
for i, tt := range tests {
got, err := NewConnectorConfigFromType(tt.typ)
if err != nil {
t.Errorf("case %d: expected nil err: %v", i, err)
continue
}
if !reflect.DeepEqual(tt.want, got) {
t.Errorf("case %d: want=%v got=%v", i, tt.want, got)
}
}
}
func TestNewConnectorConfigFromTypeUnrecognized(t *testing.T) {
_, err := NewConnectorConfigFromType("foo")
if err == nil {
t.Fatalf("Expected non-nil error")
}
}
func TestNewConnectorConfigFromMap(t *testing.T) {
user.PasswordHasher = func(plaintext string) ([]byte, error) {
return []byte(strings.ToUpper(plaintext)), nil
}
defer func() {
user.PasswordHasher = user.DefaultPasswordHasher
}()
tests := []struct {
m map[string]interface{}
want ConnectorConfig
}{
{
m: map[string]interface{}{
"type": "local",
"id": "foo",
},
want: &LocalConnectorConfig{
ID: "foo",
},
},
{
m: map[string]interface{}{
"type": "oidc",
"id": "bar",
"issuerURL": "http://example.com",
"clientID": "client123",
"clientSecret": "whaaaaa",
},
want: &OIDCConnectorConfig{
ID: "bar",
IssuerURL: "http://example.com",
ClientID: "client123",
ClientSecret: "whaaaaa",
},
},
}
for i, tt := range tests {
got, err := newConnectorConfigFromMap(tt.m)
if err != nil {
t.Errorf("case %d: want nil error: %v", i, err)
continue
}
if diff := pretty.Compare(tt.want, got); diff != "" {
t.Errorf("case %d: Compare(want, got): %v", i, diff)
}
}
}
func TestNewConnectorConfigFromMapFail(t *testing.T) {
tests := []map[string]interface{}{
// no type
map[string]interface{}{
"id": "bar",
},
// type not string
map[string]interface{}{
"id": 123,
},
}
for i, tt := range tests {
_, err := newConnectorConfigFromMap(tt)
if err == nil {
t.Errorf("case %d: want non-nil error", i)
}
}
}

View File

@ -1,100 +0,0 @@
// Package connector defines interfaces for federated identity strategies.
package connector
import (
"context"
"net/http"
)
// Connector is a mechanism for federating login to a remote identity service.
//
// Implementations are expected to implement either the PasswordConnector or
// CallbackConnector interface.
type Connector interface{}
// Scopes represents additional data requested by the clients about the end user.
type Scopes struct {
// The client has requested a refresh token from the server.
OfflineAccess bool
// The client has requested group information about the end user.
Groups bool
}
// Identity represents the ID Token claims supported by the server.
type Identity struct {
UserID string
Username string
PreferredUsername string
Email string
EmailVerified bool
Groups []string
// ConnectorData holds data used by the connector for subsequent requests after initial
// authentication, such as access tokens for upstream provides.
//
// This data is never shared with end users, OAuth clients, or through the API.
ConnectorData []byte
}
// PasswordConnector is an interface implemented by connectors which take a
// username and password.
// Prompt() is used to inform the handler what to display in the password
// template. If this returns an empty string, it'll default to "Username".
type PasswordConnector interface {
Prompt() string
Login(ctx context.Context, s Scopes, username, password string) (identity Identity, validPassword bool, err error)
}
// CallbackConnector is an interface implemented by connectors which use an OAuth
// style redirect flow to determine user information.
type CallbackConnector interface {
// The initial URL to redirect the user to.
//
// OAuth2 implementations should request different scopes from the upstream
// identity provider based on the scopes requested by the downstream client.
// For example, if the downstream client requests a refresh token from the
// server, the connector should also request a token from the provider.
//
// Many identity providers have arbitrary restrictions on refresh tokens. For
// example Google only allows a single refresh token per client/user/scopes
// combination, and wont return a refresh token even if offline access is
// requested if one has already been issues. There's no good general answer
// for these kind of restrictions, and may require this package to become more
// aware of the global set of user/connector interactions.
LoginURL(s Scopes, callbackURL, state string) (string, error)
// Handle the callback to the server and return an identity.
HandleCallback(s Scopes, r *http.Request) (identity Identity, err error)
}
// SAMLConnector represents SAML connectors which implement the HTTP POST binding.
// RelayState is handled by the server.
//
// See: https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf
// "3.5 HTTP POST Binding"
type SAMLConnector interface {
// POSTData returns an encoded SAML request and SSO URL for the server to
// render a POST form with.
//
// POSTData should encode the provided request ID in the returned serialized
// SAML request.
POSTData(s Scopes, requestID string) (ssoURL, samlRequest string, err error)
// HandlePOST decodes, verifies, and maps attributes from the SAML response.
// It passes the expected value of the "InResponseTo" response field, which
// the connector must ensure matches the response value.
//
// See: https://www.oasis-open.org/committees/download.php/35711/sstc-saml-core-errata-2.0-wd-06-diff.pdf
// "3.2.2 Complex Type StatusResponseType"
HandlePOST(s Scopes, samlResponse, inResponseTo string) (identity Identity, err error)
}
// RefreshConnector is a connector that can update the client claims.
type RefreshConnector interface {
// Refresh is called when a client attempts to claim a refresh token. The
// connector should attempt to update the identity object to reflect any
// changes since the token was last refreshed.
Refresh(ctx context.Context, s Scopes, identity Identity) (Identity, error)
}

View File

@ -0,0 +1,161 @@
package connector
import (
"encoding/json"
"fmt"
"html/template"
"net/http"
"net/url"
"path"
chttp "github.com/coreos/go-oidc/http"
"github.com/coreos/go-oidc/oauth2"
"github.com/coreos/go-oidc/oidc"
)
const (
BitbucketConnectorType = "bitbucket"
bitbucketAuthURL = "https://bitbucket.org/site/oauth2/authorize"
bitbucketTokenURL = "https://bitbucket.org/site/oauth2/access_token"
bitbucketAPIUserURL = "https://bitbucket.org/api/2.0/user"
bitbucketAPIEmailURL = "https://api.bitbucket.org/2.0/user/emails"
)
func init() {
RegisterConnectorConfigType(BitbucketConnectorType, func() ConnectorConfig { return &BitbucketConnectorConfig{} })
}
type BitbucketConnectorConfig struct {
ID string `json:"id"`
ClientID string `json:"clientID"`
ClientSecret string `json:"clientSecret"`
}
func (cfg *BitbucketConnectorConfig) ConnectorID() string {
return cfg.ID
}
func (cfg *BitbucketConnectorConfig) ConnectorType() string {
return BitbucketConnectorType
}
func (cfg *BitbucketConnectorConfig) Connector(ns url.URL, lf oidc.LoginFunc, tpls *template.Template) (Connector, error) {
ns.Path = path.Join(ns.Path, httpPathCallback)
oauth2Conn, err := newBitbucketConnector(cfg.ClientID, cfg.ClientSecret, ns.String())
if err != nil {
return nil, err
}
return &OAuth2Connector{
id: cfg.ID,
loginFunc: lf,
cbURL: ns,
conn: oauth2Conn,
}, nil
}
type bitbucketOAuth2Connector struct {
clientID string
clientSecret string
client *oauth2.Client
}
func newBitbucketConnector(clientID, clientSecret, cbURL string) (oauth2Connector, error) {
config := oauth2.Config{
Credentials: oauth2.ClientCredentials{ID: clientID, Secret: clientSecret},
AuthURL: bitbucketAuthURL,
TokenURL: bitbucketTokenURL,
AuthMethod: oauth2.AuthMethodClientSecretPost,
RedirectURL: cbURL,
}
cli, err := oauth2.NewClient(http.DefaultClient, config)
if err != nil {
return nil, err
}
return &bitbucketOAuth2Connector{
clientID: clientID,
clientSecret: clientSecret,
client: cli,
}, nil
}
func (c *bitbucketOAuth2Connector) Client() *oauth2.Client {
return c.client
}
func (c *bitbucketOAuth2Connector) Identity(cli chttp.Client) (oidc.Identity, error) {
var user struct {
UUID string `json:"uuid"`
Username string `json:"username"`
DisplayName string `json:"display_name"`
}
if err := getAndDecode(cli, bitbucketAPIUserURL, &user); err != nil {
return oidc.Identity{}, fmt.Errorf("getting user info: %v", err)
}
name := user.DisplayName
if name == "" {
name = user.Username
}
var emails struct {
Values []struct {
Email string `json:"email"`
Confirmed bool `json:"is_confirmed"`
Primary bool `json:"is_primary"`
} `json:"values"`
}
if err := getAndDecode(cli, bitbucketAPIEmailURL, &emails); err != nil {
return oidc.Identity{}, fmt.Errorf("getting user email: %v", err)
}
email := ""
for _, val := range emails.Values {
if !val.Confirmed {
continue
}
if email == "" || val.Primary {
email = val.Email
}
if val.Primary {
break
}
}
return oidc.Identity{
ID: user.UUID,
Name: name,
Email: email,
}, nil
}
func getAndDecode(cli chttp.Client, url string, v interface{}) error {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
resp, err := cli.Do(req)
if err != nil {
return fmt.Errorf("get: %v", err)
}
defer resp.Body.Close()
switch {
case resp.StatusCode >= 400 && resp.StatusCode < 500:
return oauth2.NewError(oauth2.ErrorAccessDenied)
case resp.StatusCode == http.StatusOK:
default:
return fmt.Errorf("unexpected status from providor %s", resp.Status)
}
if err := json.NewDecoder(resp.Body).Decode(v); err != nil {
return fmt.Errorf("decode body: %v", err)
}
return nil
}
func (c *bitbucketOAuth2Connector) Healthy() error {
return nil
}
func (c *bitbucketOAuth2Connector) TrustedEmailProvider() bool {
return false
}

View File

@ -0,0 +1,59 @@
package connector
import (
"net/http"
"testing"
"github.com/coreos/go-oidc/oidc"
)
var bitbucketExampleUser1 = `{
"display_name": "tutorials account",
"username": "tutorials",
"uuid": "{c788b2da-b7a2-404c-9e26-d3f077557007}"
}`
var bitbucketExampleUser2 = `{
"username": "tutorials",
"uuid": "{c788b2da-b7a2-404c-9e26-d3f077557007}"
}`
var bitbucketExampleEmail = `{
"values": [
{"email": "tutorials1@bitbucket.org","is_confirmed": false,"is_primary": false},
{"email": "tutorials2@bitbucket.org","is_confirmed": true,"is_primary": false},
{"email": "tutorials3@bitbucket.org","is_confirmed": true,"is_primary": true}
]
}`
func TestBitBucketIdentity(t *testing.T) {
tests := []oauth2IdentityTest{
{
urlResps: map[string]response{
bitbucketAPIUserURL: {http.StatusOK, bitbucketExampleUser1},
bitbucketAPIEmailURL: {http.StatusOK, bitbucketExampleEmail},
},
want: oidc.Identity{
Name: "tutorials account",
ID: "{c788b2da-b7a2-404c-9e26-d3f077557007}",
Email: "tutorials3@bitbucket.org",
},
},
{
urlResps: map[string]response{
bitbucketAPIUserURL: {http.StatusOK, bitbucketExampleUser2},
bitbucketAPIEmailURL: {http.StatusOK, bitbucketExampleEmail},
},
want: oidc.Identity{
Name: "tutorials",
ID: "{c788b2da-b7a2-404c-9e26-d3f077557007}",
Email: "tutorials3@bitbucket.org",
},
},
}
conn, err := newBitbucketConnector("fakeclientid", "fakeclientsecret", "http://example.com/auth/bitbucket/callback")
if err != nil {
t.Fatal(err)
}
runOAuth2IdentityTests(t, conn, tests)
}

View File

@ -0,0 +1,148 @@
package connector
import (
"encoding/json"
"fmt"
chttp "github.com/coreos/go-oidc/http"
"github.com/coreos/go-oidc/oauth2"
"github.com/coreos/go-oidc/oidc"
"html/template"
"net/http"
"net/url"
"path"
)
const (
FacebookConnectorType = "facebook"
facebookConnectorAuthURL = "https://www.facebook.com/dialog/oauth"
facebookTokenURL = "https://graph.facebook.com/v2.3/oauth/access_token"
facebookGraphAPIURL = "https://graph.facebook.com/me?fields=id,name,email"
)
type FacebookConnectorConfig struct {
ID string `json:"id"`
ClientID string `json:"clientID"`
ClientSecret string `json:"clientSecret"`
}
func init() {
RegisterConnectorConfigType(FacebookConnectorType, func() ConnectorConfig { return &FacebookConnectorConfig{} })
}
func (cfg *FacebookConnectorConfig) ConnectorID() string {
return cfg.ID
}
func (cfg *FacebookConnectorConfig) ConnectorType() string {
return FacebookConnectorType
}
func (cfg *FacebookConnectorConfig) Connector(ns url.URL, lf oidc.LoginFunc, tpls *template.Template) (Connector, error) {
ns.Path = path.Join(ns.Path, httpPathCallback)
oauth2Conn, err := newFacebookConnector(cfg.ClientID, cfg.ClientSecret, ns.String())
if err != nil {
return nil, err
}
return &OAuth2Connector{
id: cfg.ID,
loginFunc: lf,
cbURL: ns,
conn: oauth2Conn,
}, nil
}
type facebookOAuth2Connector struct {
clientID string
clientSecret string
client *oauth2.Client
}
func newFacebookConnector(clientID, clientSecret, cbURL string) (oauth2Connector, error) {
config := oauth2.Config{
Credentials: oauth2.ClientCredentials{ID: clientID, Secret: clientSecret},
AuthURL: facebookConnectorAuthURL,
TokenURL: facebookTokenURL,
AuthMethod: oauth2.AuthMethodClientSecretPost,
RedirectURL: cbURL,
Scope: []string{"email"},
}
cli, err := oauth2.NewClient(http.DefaultClient, config)
if err != nil {
return nil, err
}
return &facebookOAuth2Connector{
clientID: clientID,
clientSecret: clientSecret,
client: cli,
}, nil
}
func (c *facebookOAuth2Connector) Client() *oauth2.Client {
return c.client
}
func (c *facebookOAuth2Connector) Healthy() error {
return nil
}
func (c *facebookOAuth2Connector) TrustedEmailProvider() bool {
return false
}
type ErrorMessage struct {
Message string `json:"message"`
Type string `json:"type"`
Code int `json:"code"`
ErrorSubCode int `json:"error_subcode"`
ErrorUserTitle string `json:"error_user_title"`
ErrorUserMsg string `json:"error_user_msg"`
FbTraceId string `json:"fbtrace_id"`
}
type facebookErr struct {
ErrorMessage ErrorMessage `json:"error"`
}
func (err facebookErr) Error() string {
return fmt.Sprintf("facebook: %s", err.ErrorMessage.Message)
}
func (c *facebookOAuth2Connector) Identity(cli chttp.Client) (oidc.Identity, error) {
var user struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
}
req, err := http.NewRequest("GET", facebookGraphAPIURL, nil)
if err != nil {
return oidc.Identity{}, err
}
resp, err := cli.Do(req)
if err != nil {
return oidc.Identity{}, fmt.Errorf("get: %v", err)
}
defer resp.Body.Close()
switch {
case resp.StatusCode >= 400 && resp.StatusCode < 600:
var authErr facebookErr
if err := json.NewDecoder(resp.Body).Decode(&authErr); err != nil {
return oidc.Identity{}, oauth2.NewError(oauth2.ErrorAccessDenied)
}
return oidc.Identity{}, authErr
case resp.StatusCode == http.StatusOK:
default:
return oidc.Identity{}, fmt.Errorf("unexpected status from providor %s", resp.Status)
}
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return oidc.Identity{}, fmt.Errorf("decode body: %v", err)
}
return oidc.Identity{
ID: user.ID,
Name: user.Name,
Email: user.Email,
}, nil
}

View File

@ -0,0 +1,71 @@
package connector
import (
"github.com/coreos/go-oidc/oidc"
"net/http"
"testing"
)
var facebookUser1 = `{
"id":"testUser1",
"name":"testUser1Fname testUser1Lname",
"email": "testUser1@facebook.com"
}`
var facebookUser2 = `{
"id":"testUser2",
"name":"testUser2Fname testUser2Lname",
"email": "testUser2@facebook.com"
}`
var facebookExampleError = `{
"error": {
"message": "Invalid OAuth access token signature.",
"type": "OAuthException",
"code": 190,
"fbtrace_id": "Ee/6W0EfrWP"
}
}`
func TestFacebookIdentity(t *testing.T) {
tests := []oauth2IdentityTest{
{
urlResps: map[string]response{
facebookGraphAPIURL: {http.StatusOK, facebookUser1},
},
want: oidc.Identity{
Name: "testUser1Fname testUser1Lname",
ID: "testUser1",
Email: "testUser1@facebook.com",
},
},
{
urlResps: map[string]response{
facebookGraphAPIURL: {http.StatusOK, facebookUser2},
},
want: oidc.Identity{
Name: "testUser2Fname testUser2Lname",
ID: "testUser2",
Email: "testUser2@facebook.com",
},
},
{
urlResps: map[string]response{
facebookGraphAPIURL: {http.StatusUnauthorized, facebookExampleError},
},
wantErr: facebookErr{
ErrorMessage: ErrorMessage{
Code: 190,
Type: "OAuthException",
Message: "Invalid OAuth access token signature.",
FbTraceId: "Ee/6W0EfrWP",
},
},
},
}
conn, err := newFacebookConnector("fakeFacebookAppID", "fakeFacebookAppSecret", "http://example.com/auth/facebook/callback")
if err != nil {
t.Fatal(err)
}
runOAuth2IdentityTests(t, conn, tests)
}

View File

@ -0,0 +1,145 @@
package connector
import (
"encoding/json"
"fmt"
"html/template"
"net/http"
"net/url"
"path"
"strconv"
chttp "github.com/coreos/go-oidc/http"
"github.com/coreos/go-oidc/oauth2"
"github.com/coreos/go-oidc/oidc"
)
const (
GitHubConnectorType = "github"
githubAuthURL = "https://github.com/login/oauth/authorize"
githubTokenURL = "https://github.com/login/oauth/access_token"
githubAPIUserURL = "https://api.github.com/user"
)
func init() {
RegisterConnectorConfigType(GitHubConnectorType, func() ConnectorConfig { return &GitHubConnectorConfig{} })
}
type GitHubConnectorConfig struct {
ID string `json:"id"`
ClientID string `json:"clientID"`
ClientSecret string `json:"clientSecret"`
}
func (cfg *GitHubConnectorConfig) ConnectorID() string {
return cfg.ID
}
func (cfg *GitHubConnectorConfig) ConnectorType() string {
return GitHubConnectorType
}
func (cfg *GitHubConnectorConfig) Connector(ns url.URL, lf oidc.LoginFunc, tpls *template.Template) (Connector, error) {
ns.Path = path.Join(ns.Path, httpPathCallback)
oauth2Conn, err := newGitHubConnector(cfg.ClientID, cfg.ClientSecret, ns.String())
if err != nil {
return nil, err
}
return &OAuth2Connector{
id: cfg.ID,
loginFunc: lf,
cbURL: ns,
conn: oauth2Conn,
}, nil
}
type githubOAuth2Connector struct {
clientID string
clientSecret string
client *oauth2.Client
}
func newGitHubConnector(clientID, clientSecret, cbURL string) (oauth2Connector, error) {
config := oauth2.Config{
Credentials: oauth2.ClientCredentials{ID: clientID, Secret: clientSecret},
AuthURL: githubAuthURL,
TokenURL: githubTokenURL,
Scope: []string{"user:email"},
AuthMethod: oauth2.AuthMethodClientSecretPost,
RedirectURL: cbURL,
}
cli, err := oauth2.NewClient(http.DefaultClient, config)
if err != nil {
return nil, err
}
return &githubOAuth2Connector{
clientID: clientID,
clientSecret: clientSecret,
client: cli,
}, nil
}
// standard error form returned by github
type githubError struct {
Message string `json:"message"`
}
func (err githubError) Error() string {
return fmt.Sprintf("github: %s", err.Message)
}
func (c *githubOAuth2Connector) Client() *oauth2.Client {
return c.client
}
func (c *githubOAuth2Connector) Identity(cli chttp.Client) (oidc.Identity, error) {
req, err := http.NewRequest("GET", githubAPIUserURL, nil)
if err != nil {
return oidc.Identity{}, err
}
resp, err := cli.Do(req)
if err != nil {
return oidc.Identity{}, fmt.Errorf("get: %v", err)
}
defer resp.Body.Close()
switch {
case resp.StatusCode >= 400 && resp.StatusCode < 600:
// attempt to decode error from github
var authErr githubError
if err := json.NewDecoder(resp.Body).Decode(&authErr); err != nil {
return oidc.Identity{}, oauth2.NewError(oauth2.ErrorAccessDenied)
}
return oidc.Identity{}, authErr
case resp.StatusCode == http.StatusOK:
default:
return oidc.Identity{}, fmt.Errorf("unexpected status from providor %s", resp.Status)
}
var user struct {
Login string `json:"login"`
ID int64 `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
}
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return oidc.Identity{}, fmt.Errorf("getting user info: %v", err)
}
name := user.Name
if name == "" {
name = user.Login
}
return oidc.Identity{
ID: strconv.FormatInt(user.ID, 10),
Name: name,
Email: user.Email,
}, nil
}
func (c *githubOAuth2Connector) Healthy() error {
return nil
}
func (c *githubOAuth2Connector) TrustedEmailProvider() bool {
return false
}

View File

@ -0,0 +1,41 @@
package connector
import (
"net/http"
"testing"
"github.com/coreos/go-oidc/oidc"
)
var (
githubExampleUser = `{"login":"octocat","id":1,"name": "monalisa octocat","email": "octocat@github.com"}`
githubExampleError = `{"message":"Bad credentials","documentation_url":"https://developer.github.com/v3"}`
)
func TestGitHubIdentity(t *testing.T) {
tests := []oauth2IdentityTest{
{
urlResps: map[string]response{
githubAPIUserURL: {http.StatusOK, githubExampleUser},
},
want: oidc.Identity{
Name: "monalisa octocat",
ID: "1",
Email: "octocat@github.com",
},
},
{
urlResps: map[string]response{
githubAPIUserURL: {http.StatusUnauthorized, githubExampleError},
},
wantErr: githubError{
Message: "Bad credentials",
},
},
}
conn, err := newGitHubConnector("fakeclientid", "fakeclientsecret", "http://examle.com/auth/github/callback")
if err != nil {
t.Fatal(err)
}
runOAuth2IdentityTests(t, conn, tests)
}

570
connector/connector_ldap.go Normal file
View File

@ -0,0 +1,570 @@
package connector
import (
"crypto/tls"
"crypto/x509"
"errors"
"net"
"fmt"
"html/template"
"io/ioutil"
"net/http"
"net/url"
"path"
"strings"
"sync"
"time"
"github.com/coreos/dex/pkg/log"
"github.com/coreos/go-oidc/oidc"
"gopkg.in/ldap.v2"
)
const (
LDAPConnectorType = "ldap"
LDAPLoginPageTemplateName = "ldap-login.html"
)
func init() {
RegisterConnectorConfigType(LDAPConnectorType, func() ConnectorConfig { return &LDAPConnectorConfig{} })
// Set default ldap timeout.
ldap.DefaultTimeout = 30 * time.Second
}
type LDAPConnectorConfig struct {
ID string `json:"id"`
// Host and port of ldap service in form "host:port"
Host string `json:"host"`
// UseTLS indicates that the connector should use the TLS port.
UseTLS bool `json:"useTLS"`
UseSSL bool `json:"useSSL"`
// Trusted TLS certificate when connecting to the LDAP server. If empty the
// host's root certificates will be used.
CaFile string `json:"caFile"`
// CertFile and KeyFile are used to specifiy client certificate data.
CertFile string `json:"certFile"`
KeyFile string `json:"keyFile"`
MaxIdleConn int `json:"maxIdleConn"`
NameAttribute string `json:"nameAttribute"`
EmailAttribute string `json:"emailAttribute"`
// The place to start all searches from.
BaseDN string `json:"baseDN"`
// Search fields indicate how to search for user records in LDAP.
SearchBeforeAuth bool `json:"searchBeforeAuth"`
SearchFilter string `json:"searchFilter"`
SearchScope string `json:"searchScope"`
SearchBindDN string `json:"searchBindDN"`
SearchBindPw string `json:"searchBindPw"`
SearchGroupFilter string `json:"searchGroupFilter"`
// BindTemplate is a format string that maps user names to a record to bind as.
// It's passed both the username entered by the end user and the base DN.
//
// For example the bindTemplate
//
// "uid=%u,%d"
//
// with the username "johndoe" and basename "ou=People,dc=example,dc=com" would attempt
// to bind as
//
// "uid=johndoe,ou=People,dc=example,dc=com"
//
BindTemplate string `json:"bindTemplate"`
// DEPRICATED fields that exist for backward compatibility.
// Use "host" instead of "ServerHost" and "ServerPort"
ServerHost string `json:"serverHost"`
ServerPort uint16 `json:"serverPort"`
Timeout time.Duration `json:"timeout"`
}
func (cfg *LDAPConnectorConfig) ConnectorID() string {
return cfg.ID
}
func (cfg *LDAPConnectorConfig) ConnectorType() string {
return LDAPConnectorType
}
type LDAPConnector struct {
id string
namespace url.URL
loginFunc oidc.LoginFunc
loginTpl *template.Template
baseDN string
nameAttribute string
emailAttribute string
searchBeforeAuth bool
searchFilter string
searchScope int
searchBindDN string
searchBindPw string
searchGroupFilter string
bindTemplate string
ldapPool *LDAPPool
}
const defaultPoolCheckTimer = 7200 * time.Second
func (cfg *LDAPConnectorConfig) Connector(ns url.URL, lf oidc.LoginFunc, tpls *template.Template) (Connector, error) {
ns.Path = path.Join(ns.Path, httpPathCallback)
tpl := tpls.Lookup(LDAPLoginPageTemplateName)
if tpl == nil {
return nil, fmt.Errorf("unable to find necessary HTML template")
}
if cfg.UseTLS && cfg.UseSSL {
return nil, fmt.Errorf("Invalid configuration. useTLS and useSSL are mutual exclusive.")
}
if len(cfg.CertFile) > 0 && len(cfg.KeyFile) == 0 {
return nil, fmt.Errorf("Invalid configuration. Both certFile and keyFile must be specified.")
}
// Set default values
if cfg.NameAttribute == "" {
cfg.NameAttribute = "cn"
}
if cfg.EmailAttribute == "" {
cfg.EmailAttribute = "mail"
}
if cfg.MaxIdleConn > 0 {
cfg.MaxIdleConn = 5
}
if cfg.BindTemplate == "" {
cfg.BindTemplate = "uid=%u,%b"
} else if cfg.SearchBeforeAuth {
log.Warningf("bindTemplate not used when searchBeforeAuth specified.")
}
searchScope := ldap.ScopeWholeSubtree
if cfg.SearchScope != "" {
switch {
case strings.EqualFold(cfg.SearchScope, "BASE"):
searchScope = ldap.ScopeBaseObject
case strings.EqualFold(cfg.SearchScope, "ONE"):
searchScope = ldap.ScopeSingleLevel
case strings.EqualFold(cfg.SearchScope, "SUB"):
searchScope = ldap.ScopeWholeSubtree
default:
return nil, fmt.Errorf("Invalid value for searchScope: '%v'. Must be one of 'base', 'one' or 'sub'.", cfg.SearchScope)
}
}
if cfg.Host == "" {
if cfg.ServerHost == "" {
return nil, errors.New("no host provided")
}
// For backward compatibility construct host form old fields.
cfg.Host = fmt.Sprintf("%s:%d", cfg.ServerHost, cfg.ServerPort)
}
host, _, err := net.SplitHostPort(cfg.Host)
if err != nil {
return nil, fmt.Errorf("host is not of form 'host:port': %v", err)
}
tlsConfig := &tls.Config{ServerName: host}
if (cfg.UseTLS || cfg.UseSSL) && len(cfg.CaFile) > 0 {
buf, err := ioutil.ReadFile(cfg.CaFile)
if err != nil {
return nil, err
}
rootCertPool := x509.NewCertPool()
ok := rootCertPool.AppendCertsFromPEM(buf)
if ok {
tlsConfig.RootCAs = rootCertPool
} else {
return nil, fmt.Errorf("%v: Unable to parse certificate data.", cfg.CaFile)
}
}
if (cfg.UseTLS || cfg.UseSSL) && len(cfg.CertFile) > 0 && len(cfg.KeyFile) > 0 {
cert, err := tls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
}
idpc := &LDAPConnector{
id: cfg.ID,
namespace: ns,
loginFunc: lf,
loginTpl: tpl,
baseDN: cfg.BaseDN,
nameAttribute: cfg.NameAttribute,
emailAttribute: cfg.EmailAttribute,
searchBeforeAuth: cfg.SearchBeforeAuth,
searchFilter: cfg.SearchFilter,
searchGroupFilter: cfg.SearchGroupFilter,
searchScope: searchScope,
searchBindDN: cfg.SearchBindDN,
searchBindPw: cfg.SearchBindPw,
bindTemplate: cfg.BindTemplate,
ldapPool: &LDAPPool{
MaxIdleConn: cfg.MaxIdleConn,
PoolCheckTimer: defaultPoolCheckTimer,
Host: cfg.Host,
UseTLS: cfg.UseTLS,
UseSSL: cfg.UseSSL,
TLSConfig: tlsConfig,
},
}
return idpc, nil
}
func (c *LDAPConnector) ID() string {
return c.id
}
func (c *LDAPConnector) Healthy() error {
return c.ldapPool.Do(func(c *ldap.Conn) error {
// Attempt an anonymous bind.
return c.Bind("", "")
})
}
func (c *LDAPConnector) LoginURL(sessionKey, prompt string) (string, error) {
q := url.Values{}
q.Set("session_key", sessionKey)
q.Set("prompt", prompt)
enc := q.Encode()
return path.Join(c.namespace.Path, "login") + "?" + enc, nil
}
func (c *LDAPConnector) Handler(errorURL url.URL) http.Handler {
route := path.Join(c.namespace.Path, "/login")
return handlePasswordLogin(c.loginFunc, c.loginTpl, c, route, errorURL)
}
func (c *LDAPConnector) Sync() chan struct{} {
stop := make(chan struct{})
go func() {
for {
select {
case <-time.After(c.ldapPool.PoolCheckTimer):
alive, killed := c.ldapPool.CheckConnections()
if alive > 0 {
log.Infof("Connector ID=%v idle_conns=%v", c.id, alive)
}
if killed > 0 {
log.Warningf("Connector ID=%v closed %v dead connections.", c.id, killed)
}
case <-stop:
return
}
}
}()
return stop
}
func (c *LDAPConnector) TrustedEmailProvider() bool {
return true
}
// A LDAPPool is a Connection Pool for LDAP connections. Use Do() to request connections
// from the pool.
type LDAPPool struct {
m sync.Mutex
conns map[*ldap.Conn]struct{}
MaxIdleConn int
PoolCheckTimer time.Duration
Host string
UseTLS bool
UseSSL bool
TLSConfig *tls.Config
}
// Do runs a function which requires an LDAP connection.
//
// The connection will be unauthenticated with the server and should not be closed by f.
func (p *LDAPPool) Do(f func(conn *ldap.Conn) error) (err error) {
conn := p.removeRandomConn()
if conn == nil {
conn, err = p.ldapConnect()
if err != nil {
return err
}
}
defer p.put(conn)
return f(conn)
}
// put makes a connection ready for re-use and puts it back into the pool. If the connection
// cannot be reused it is discarded. If there already are MaxIdleConn connections in the pool
// the connection is discarded.
func (p *LDAPPool) put(c *ldap.Conn) {
p.m.Lock()
if p.conns == nil {
// First call to Put, initialize map
p.conns = make(map[*ldap.Conn]struct{})
}
if len(p.conns)+1 > p.MaxIdleConn {
p.m.Unlock()
c.Close()
return
}
p.m.Unlock()
// drop to anonymous bind
err := c.Bind("", "")
if err != nil {
// unsupported or disallowed, throw away connection
log.Warningf("Unable to re-use LDAP Connection after failure to bind anonymously: %v", err)
c.Close()
return
}
p.m.Lock()
p.conns[c] = struct{}{}
p.m.Unlock()
}
// removeConn attempts to remove the provided connection from the pool. If removeConn returns false
// another routine is using the connection and the caller should discard the pointer.
func (p *LDAPPool) removeConn(conn *ldap.Conn) bool {
p.m.Lock()
_, ok := p.conns[conn]
delete(p.conns, conn)
p.m.Unlock()
return ok
}
// removeRandomConn attempts to remove a random connection from the pool. If removeRandomConn
// returns nil the pool is empty.
func (p *LDAPPool) removeRandomConn() *ldap.Conn {
p.m.Lock()
defer p.m.Unlock()
for conn := range p.conns {
delete(p.conns, conn)
return conn
}
return nil
}
// CheckConnections attempts to iterate over all the connections in the pool and check wheter
// they are alive or not. Live connections are put back into the pool, dead ones are discarded.
func (p *LDAPPool) CheckConnections() (int, int) {
var conns []*ldap.Conn
var alive, killed int
// Get snapshot of connection-map while holding Lock
p.m.Lock()
for conn := range p.conns {
conns = append(conns, conn)
}
p.m.Unlock()
// Iterate over snapshot, Get and ping connections.
// Put live connections back into pool, Close dead ones.
for _, conn := range conns {
ok := p.removeConn(conn)
if ok {
err := ldapPing(conn)
if err == nil {
p.put(conn)
alive++
} else {
conn.Close()
killed++
}
}
}
return alive, killed
}
func ldapPing(conn *ldap.Conn) error {
// Query root DSE
s := ldap.NewSearchRequest("", ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, "(objectClass=*)", []string{}, nil)
_, err := conn.Search(s)
return err
}
func (p *LDAPPool) ldapConnect() (*ldap.Conn, error) {
var err error
var ldapConn *ldap.Conn
if p.UseSSL {
ldapConn, err = ldap.DialTLS("tcp", p.Host, p.TLSConfig)
if err != nil {
return nil, err
}
} else {
ldapConn, err = ldap.Dial("tcp", p.Host)
if err != nil {
return nil, err
}
if p.UseTLS {
err = ldapConn.StartTLS(p.TLSConfig)
if err != nil {
return nil, err
}
}
}
return ldapConn, err
}
// invalidBindCredentials determines if a bind error was the result of invalid
// credentials.
func invalidBindCredentials(err error) bool {
ldapErr, ok := err.(*ldap.Error)
if ok {
return false
}
return ldapErr.ResultCode == ldap.LDAPResultInvalidCredentials
}
func (c *LDAPConnector) formatDN(template, username string) string {
result := template
result = strings.Replace(result, "%u", ldap.EscapeFilter(username), -1)
result = strings.Replace(result, "%b", c.baseDN, -1)
return result
}
func (c *LDAPConnector) Groups(fullUserID string) ([]string, error) {
if !c.searchBeforeAuth {
return nil, fmt.Errorf("cannot search without service account")
}
if c.searchGroupFilter == "" {
return nil, fmt.Errorf("no group filter specified")
}
var groups []string
err := c.ldapPool.Do(func(conn *ldap.Conn) error {
if err := conn.Bind(c.searchBindDN, c.searchBindPw); err != nil {
if !invalidBindCredentials(err) {
log.Errorf("failed to connect to LDAP for search bind: %v", err)
}
return fmt.Errorf("failed to bind: %v", err)
}
req := &ldap.SearchRequest{
BaseDN: c.baseDN,
Scope: c.searchScope,
Filter: c.formatDN(c.searchGroupFilter, fullUserID),
}
resp, err := conn.Search(req)
if err != nil {
return fmt.Errorf("search failed: %v", err)
}
groups = make([]string, len(resp.Entries))
for i, entry := range resp.Entries {
groups[i] = entry.DN
}
return nil
})
return groups, err
}
func (c *LDAPConnector) Identity(username, password string) (*oidc.Identity, error) {
var (
identity *oidc.Identity
err error
)
if c.searchBeforeAuth {
err = c.ldapPool.Do(func(conn *ldap.Conn) error {
if err := conn.Bind(c.searchBindDN, c.searchBindPw); err != nil {
if !invalidBindCredentials(err) {
log.Errorf("failed to connect to LDAP for search bind: %v", err)
}
return fmt.Errorf("failed to bind: %v", err)
}
filter := c.formatDN(c.searchFilter, username)
req := &ldap.SearchRequest{
BaseDN: c.baseDN,
Scope: c.searchScope,
Filter: filter,
Attributes: []string{c.nameAttribute, c.emailAttribute},
}
resp, err := conn.Search(req)
if err != nil {
return fmt.Errorf("search failed: %v", err)
}
switch len(resp.Entries) {
case 0:
return errors.New("user not found by search")
case 1:
default:
// For now reject searches that return multiple entries to avoid ambiguity.
log.Errorf("LDAP search %q returned %d entries. Must disambiguate searchFilter.", filter, len(resp.Entries))
return errors.New("search returned multiple entries")
}
entry := resp.Entries[0]
email := entry.GetAttributeValue(c.emailAttribute)
if email == "" {
return fmt.Errorf("no email attribute found")
}
identity = &oidc.Identity{
ID: entry.DN,
Name: entry.GetAttributeValue(c.nameAttribute),
Email: email,
}
// Attempt to bind as the end user.
return conn.Bind(entry.DN, password)
})
} else {
err = c.ldapPool.Do(func(conn *ldap.Conn) error {
userBindDN := c.formatDN(c.bindTemplate, username)
if err := conn.Bind(userBindDN, password); err != nil {
if !invalidBindCredentials(err) {
log.Errorf("failed to connect to LDAP for search bind: %v", err)
}
return fmt.Errorf("failed to bind: %v", err)
}
req := &ldap.SearchRequest{
BaseDN: userBindDN,
Scope: ldap.ScopeBaseObject, // Only attempt to
Filter: "(objectClass=*)",
}
resp, err := conn.Search(req)
if err != nil {
return fmt.Errorf("search failed: %v", err)
}
if len(resp.Entries) == 0 {
// Are there cases were a user wouldn't be able to see their own entity?
return fmt.Errorf("user not found by search")
}
entry := resp.Entries[0]
email := entry.GetAttributeValue(c.emailAttribute)
if email == "" {
return fmt.Errorf("no email attribute found")
}
identity = &oidc.Identity{
ID: entry.DN,
Name: entry.GetAttributeValue(c.nameAttribute),
Email: email,
}
return nil
})
}
if err != nil {
return nil, err
}
return identity, nil
}

View File

@ -0,0 +1,100 @@
package connector
import (
"html/template"
"net/url"
"testing"
"github.com/coreos/go-oidc/oidc"
)
var (
ns url.URL
lf oidc.LoginFunc
templates *template.Template
)
func init() {
templates = template.New(LDAPLoginPageTemplateName)
}
func TestLDAPConnectorConfigValidTLS(t *testing.T) {
cc := LDAPConnectorConfig{
ID: "ldap",
Host: "example.com:636",
UseTLS: true,
UseSSL: false,
}
_, err := cc.Connector(ns, lf, templates)
if err != nil {
t.Fatal(err)
}
}
func TestLDAPConnectorConfigInvalidSSLandTLS(t *testing.T) {
cc := LDAPConnectorConfig{
ID: "ldap",
Host: "example.com:636",
UseTLS: true,
UseSSL: true,
}
_, err := cc.Connector(ns, lf, templates)
if err == nil {
t.Fatal("Expected LDAPConnector initialization to fail when both TLS and SSL enabled.")
}
}
func TestLDAPConnectorConfigValidSearchScope(t *testing.T) {
cc := LDAPConnectorConfig{
ID: "ldap",
Host: "example.com:636",
SearchScope: "one",
}
_, err := cc.Connector(ns, lf, templates)
if err != nil {
t.Fatal(err)
}
}
func TestLDAPConnectorConfigInvalidSearchScope(t *testing.T) {
cc := LDAPConnectorConfig{
ID: "ldap",
Host: "example.com:636",
SearchScope: "three",
}
_, err := cc.Connector(ns, lf, templates)
if err == nil {
t.Fatal("Expected LDAPConnector initialization to fail when invalid value provided for SearchScope.")
}
}
func TestLDAPConnectorConfigInvalidCertFileNoKeyFile(t *testing.T) {
cc := LDAPConnectorConfig{
ID: "ldap",
Host: "example.com:636",
CertFile: "/tmp/ldap.crt",
}
_, err := cc.Connector(ns, lf, templates)
if err == nil {
t.Fatal("Expected LDAPConnector initialization to fail when CertFile specified without KeyFile.")
}
}
func TestLDAPConnectorConfigValidCertFileAndKeyFile(t *testing.T) {
cc := LDAPConnectorConfig{
ID: "ldap",
Host: "example.com:636",
CertFile: "/tmp/ldap.crt",
KeyFile: "/tmp/ldap.key",
}
_, err := cc.Connector(ns, lf, templates)
if err != nil {
t.Fatal(err)
}
}

View File

@ -0,0 +1,120 @@
package connector
import (
"fmt"
"html/template"
"net/http"
"net/url"
"path"
"github.com/coreos/dex/user"
"github.com/coreos/go-oidc/oidc"
)
const (
LocalConnectorType = "local"
LoginPageTemplateName = "local-login.html"
)
func init() {
RegisterConnectorConfigType(LocalConnectorType, func() ConnectorConfig { return &LocalConnectorConfig{} })
}
type LocalConnectorConfig struct {
ID string `json:"id"`
}
func (cfg *LocalConnectorConfig) ConnectorID() string {
return cfg.ID
}
func (cfg *LocalConnectorConfig) ConnectorType() string {
return LocalConnectorType
}
func (cfg *LocalConnectorConfig) Connector(ns url.URL, lf oidc.LoginFunc, tpls *template.Template) (Connector, error) {
tpl := tpls.Lookup(LoginPageTemplateName)
if tpl == nil {
return nil, fmt.Errorf("unable to find necessary HTML template")
}
idpc := &LocalConnector{
id: cfg.ID,
namespace: ns,
loginFunc: lf,
loginTpl: tpl,
}
return idpc, nil
}
type LocalConnector struct {
id string
idp *LocalIdentityProvider
namespace url.URL
loginFunc oidc.LoginFunc
loginTpl *template.Template
}
type Page struct {
PostURL string
Name string
Error bool
Message string
SessionKey string
}
func (c *LocalConnector) ID() string {
return c.id
}
func (c *LocalConnector) Healthy() error {
return nil
}
func (c *LocalConnector) SetLocalIdentityProvider(idp *LocalIdentityProvider) {
c.idp = idp
}
func (c *LocalConnector) LoginURL(sessionKey, prompt string) (string, error) {
q := url.Values{}
q.Set("session_key", sessionKey)
q.Set("prompt", prompt)
enc := q.Encode()
return path.Join(c.namespace.Path, "login") + "?" + enc, nil
}
func (c *LocalConnector) Handler(errorURL url.URL) http.Handler {
route := path.Join(c.namespace.Path, "/login")
return handlePasswordLogin(c.loginFunc, c.loginTpl, c.idp, route, errorURL)
}
func (c *LocalConnector) Sync() chan struct{} {
return make(chan struct{})
}
func (c *LocalConnector) TrustedEmailProvider() bool {
return false
}
type LocalIdentityProvider struct {
PasswordInfoRepo user.PasswordInfoRepo
UserRepo user.UserRepo
}
func (m *LocalIdentityProvider) Identity(email, password string) (*oidc.Identity, error) {
user, err := m.UserRepo.GetByEmail(nil, email)
if err != nil {
return nil, err
}
id := user.ID
pi, err := m.PasswordInfoRepo.Get(nil, id)
if err != nil {
return nil, err
}
return pi.Authenticate(password)
}

View File

@ -0,0 +1,140 @@
package connector
import (
"net/http"
"net/url"
"strings"
"github.com/coreos/dex/pkg/log"
chttp "github.com/coreos/go-oidc/http"
"github.com/coreos/go-oidc/oauth2"
"github.com/coreos/go-oidc/oidc"
)
type oauth2Connector interface {
Client() *oauth2.Client
// Identity uses a HTTP client authenticated as the end user to construct
// an OIDC identity for that user.
Identity(cli chttp.Client) (oidc.Identity, error)
// Healthy it should attempt to determine if the connector's credientials
// are valid.
Healthy() error
TrustedEmailProvider() bool
}
type OAuth2Connector struct {
id string
loginFunc oidc.LoginFunc
cbURL url.URL
conn oauth2Connector
}
func (c *OAuth2Connector) ID() string {
return c.id
}
func (c *OAuth2Connector) Healthy() error {
return c.conn.Healthy()
}
func (c *OAuth2Connector) Sync() chan struct{} {
stop := make(chan struct{}, 1)
return stop
}
func (c *OAuth2Connector) TrustedEmailProvider() bool {
return c.conn.TrustedEmailProvider()
}
func (c *OAuth2Connector) LoginURL(sessionKey, prompt string) (string, error) {
return c.conn.Client().AuthCodeURL(sessionKey, oauth2.GrantTypeAuthCode, prompt), nil
}
func (c *OAuth2Connector) Handler(errorURL url.URL) http.Handler {
return c.handleCallbackFunc(c.loginFunc, errorURL)
}
func (c *OAuth2Connector) handleCallbackFunc(lf oidc.LoginFunc, errorURL url.URL) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
e := q.Get("error")
if e != "" {
redirectError(w, errorURL, q)
return
}
code := q.Get("code")
if code == "" {
q.Set("error", oauth2.ErrorInvalidRequest)
q.Set("error_description", "code query param must be set")
redirectError(w, errorURL, q)
return
}
sessionKey := q.Get("state")
token, err := c.conn.Client().RequestToken(oauth2.GrantTypeAuthCode, code)
if err != nil {
log.Errorf("Unable to verify auth code with issuer: %v", err)
q.Set("error", oauth2.ErrorUnsupportedResponseType)
q.Set("error_description", "unable to verify auth code with issuer")
redirectError(w, errorURL, q)
return
}
ident, err := c.conn.Identity(newAuthenticatedClient(token, http.DefaultClient))
if err != nil {
log.Errorf("Unable to retrieve identity: %v", err)
q.Set("error", oauth2.ErrorUnsupportedResponseType)
q.Set("error_description", "unable to retrieve identity from issuer")
redirectError(w, errorURL, q)
return
}
redirectURL, err := lf(ident, sessionKey)
if err != nil {
log.Errorf("Unable to log in %#v: %v", ident, err)
q.Set("error", oauth2.ErrorAccessDenied)
q.Set("error_description", "login failed")
redirectError(w, errorURL, q)
return
}
w.Header().Set("Location", redirectURL)
w.WriteHeader(http.StatusFound)
return
}
}
// authedClient authenticates all requests as the end user.
type authedClient struct {
token oauth2.TokenResponse
cli chttp.Client
}
func newAuthenticatedClient(token oauth2.TokenResponse, cli chttp.Client) chttp.Client {
return &authedClient{token, cli}
}
func (c *authedClient) Do(req *http.Request) (*http.Response, error) {
req.Header.Set("Authorization", tokenType(c.token)+" "+c.token.AccessToken)
return c.cli.Do(req)
}
// Return the canonical name of the token type if non-empty, else "Bearer".
// Take from golang.org/x/oauth2
func tokenType(token oauth2.TokenResponse) string {
if strings.EqualFold(token.TokenType, "bearer") {
return "Bearer"
}
if strings.EqualFold(token.TokenType, "mac") {
return "MAC"
}
if strings.EqualFold(token.TokenType, "basic") {
return "Basic"
}
if token.TokenType != "" {
return token.TokenType
}
return "Bearer"
}

Some files were not shown because too many files have changed in this diff Show More