Merge pull request #471 from bobbyrullo/native

Implement Public Clients
This commit is contained in:
bobbyrullo 2016-06-20 17:03:39 -07:00 committed by GitHub
commit 3b8d704c9c
29 changed files with 763 additions and 116 deletions

58
Documentation/clients.md Normal file
View file

@ -0,0 +1,58 @@
# 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.

View file

@ -69,10 +69,15 @@ func errorMaker(typ string, desc string, code int) func(internal error) Error {
var (
ErrorMissingClient = errorMaker("bad_request", "The 'client' cannot be empty", http.StatusBadRequest)(nil)
// Called when oidc.ClientMetadata.Valid() fails.
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),
user.ErrorNotFound: errorMaker("resource_not_found", "Resource could not be found.", http.StatusNotFound),
user.ErrorDuplicateEmail: errorMaker("bad_request", "Email already in use.", http.StatusBadRequest),
user.ErrorInvalidEmail: errorMaker("bad_request", "invalid email.", http.StatusBadRequest),
@ -86,7 +91,6 @@ var (
func (a *AdminAPI) GetAdmin(id string) (adminschema.Admin, error) {
usr, err := a.userRepo.Get(nil, id)
if err != nil {
return adminschema.Admin{}, mapError(err)
}
@ -136,15 +140,9 @@ func (a *AdminAPI) CreateClient(req adminschema.ClientCreateRequest) (adminschem
return adminschema.ClientCreateResponse{}, mapError(err)
}
if err := cli.Metadata.Valid(); err != nil {
return adminschema.ClientCreateResponse{}, ErrorInvalidClientFunc(err)
}
// metadata is guaranteed to have at least one redirect_uri by earlier validation.
creds, err := a.clientManager.New(cli, &clientmanager.ClientOptions{
TrustedPeers: req.Client.TrustedPeers,
})
if err != nil {
return adminschema.ClientCreateResponse{}, mapError(err)
}
@ -165,6 +163,12 @@ func (a *AdminAPI) GetConnectors() ([]connector.ConnectorConfig, error) {
}
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)
}

View file

@ -7,6 +7,7 @@ import (
"io"
"net/url"
"reflect"
"strings"
"golang.org/x/crypto/bcrypt"
@ -19,11 +20,27 @@ var (
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.")
ErrorNotFound = errors.New("no data found")
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) {
@ -44,6 +61,35 @@ 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 {
@ -106,6 +152,7 @@ func ClientsFromReader(r io.Reader) ([]LoadableClient, error) {
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 {
@ -137,7 +184,8 @@ func ClientsFromReader(r io.Reader) ([]LoadableClient, error) {
Metadata: oidc.ClientMetadata{
RedirectURIs: redirectURIs,
},
Admin: client.Admin,
Admin: client.Admin,
Public: client.Public,
},
TrustedPeers: client.TrustedPeers,
}

View file

@ -35,6 +35,13 @@ var (
"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 + `",
@ -139,6 +146,26 @@ func TestClientsFromReader(t *testing.T) {
},
},
},
{
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,
@ -177,7 +204,101 @@ func TestClientsFromReader(t *testing.T) {
}
}
}
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 {

View file

@ -2,6 +2,7 @@ package manager
import (
"encoding/base64"
"net/url"
"errors"
@ -21,6 +22,10 @@ const (
maxSecretLength = 72
)
var (
localHostRedirectURL = mustParseURL("http://localhost:0")
)
type ClientOptions struct {
TrustedPeers []string
}
@ -67,6 +72,10 @@ func NewClientManager(clientRepo client.ClientRepo, txnFactory repo.TransactionF
}
}
// 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 {
@ -74,11 +83,14 @@ func (m *ClientManager) New(cli client.Client, options *ClientOptions) (*oidc.Cl
}
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
@ -156,7 +168,13 @@ func (m *ClientManager) SetDexAdmin(clientID string, isAdmin bool) error {
func (m *ClientManager) Authenticate(creds oidc.ClientCredentials) (bool, error) {
clientSecret, err := m.clientRepo.GetSecret(nil, creds.ID)
if err != nil || clientSecret == nil {
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
}
@ -171,11 +189,15 @@ func (m *ClientManager) Authenticate(creds oidc.ClientCredentials) (bool, error)
}
func (m *ClientManager) addClientCredentials(cli *client.Client) error {
// Generate Client ID
if len(cli.Metadata.RedirectURIs) < 1 {
return errors.New("no client redirect url given")
var seed string
if cli.Public {
seed = cli.Metadata.ClientName
} else {
seed = cli.Metadata.RedirectURIs[0].Host
}
clientID, err := m.clientIDGenerator(cli.Metadata.RedirectURIs[0].Host)
// Generate Client ID
clientID, err := m.clientIDGenerator(seed)
if err != nil {
return err
}
@ -192,3 +214,37 @@ func (m *ClientManager) addClientCredentials(cli *client.Client) error {
}
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

@ -168,3 +168,53 @@ func TestAuthenticate(t *testing.T) {
}
}
}
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)
}
}
}

View file

@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"net/url"
"reflect"
"github.com/coreos/go-oidc/oidc"
@ -23,6 +24,10 @@ const (
pgErrorCodeUniqueViolation = "23505" // unique_violation
)
var (
localHostRedirectURL = mustParseURL("http://localhost:0")
)
func init() {
register(table{
name: clientTableName,
@ -44,6 +49,16 @@ func newClientModel(cli client.Client) (*clientModel, error) {
if err != nil {
return nil, err
}
if cli.Public {
// Metadata.Valid(), and therefore json.Unmarshal(metadata) complains
// when there's no RedirectURIs, so we set them to a fixed value here,
// and remove it when translating back to a client.Client
cli.Metadata.RedirectURIs = []url.URL{
localHostRedirectURL,
}
}
bmeta, err := json.Marshal(&cli.Metadata)
if err != nil {
return nil, err
@ -54,6 +69,7 @@ func newClientModel(cli client.Client) (*clientModel, error) {
Secret: hashed,
Metadata: string(bmeta),
DexAdmin: cli.Admin,
Public: cli.Public,
}
return &cim, nil
@ -64,6 +80,7 @@ type clientModel struct {
Secret []byte `db:"secret"`
Metadata string `db:"metadata"`
DexAdmin bool `db:"dex_admin"`
Public bool `db:"public"`
}
type trustedPeerModel struct {
@ -76,13 +93,18 @@ func (m *clientModel) Client() (*client.Client, error) {
Credentials: oidc.ClientCredentials{
ID: m.ID,
},
Admin: m.DexAdmin,
Admin: m.DexAdmin,
Public: m.Public,
}
if err := json.Unmarshal([]byte(m.Metadata), &ci.Metadata); err != nil {
return nil, err
}
if ci.Public {
ci.Metadata.RedirectURIs = []url.URL{}
}
return &ci, nil
}
@ -168,7 +190,6 @@ func isAlreadyExistsErr(err error) bool {
func (r *clientRepo) New(tx repo.Transaction, cli client.Client) (*oidc.ClientCredentials, error) {
cim, err := newClientModel(cli)
if err != nil {
return nil, err
}
@ -328,3 +349,11 @@ func (r *clientRepo) SetTrustedPeers(tx repo.Transaction, clientID string, clien
return nil
}
func mustParseURL(s string) url.URL {
u, err := url.Parse(s)
if err != nil {
panic(err)
}
return *u
}

View file

@ -16,7 +16,8 @@ CREATE TABLE client_identity (
id text NOT NULL UNIQUE,
secret blob,
metadata text,
dex_admin integer
dex_admin integer,
public integer
);
CREATE TABLE connector_config (

View file

@ -48,6 +48,22 @@ func TestGetPlannedMigrations(t *testing.T) {
}
func TestMigrateClientMetadata(t *testing.T) {
// oldClientModel exists to model what the client model looked like at
// migration time. Without using this, the test fails because there's no
// columns for the new fields.
type oldClientModel struct {
ID string `db:"id"`
Secret []byte `db:"secret"`
Metadata string `db:"metadata"`
DexAdmin bool `db:"dex_admin"`
}
register(table{
name: clientTableName,
model: oldClientModel{},
autoinc: false,
pkey: []string{"id"},
})
dsn := os.Getenv("DEX_TEST_DSN")
if dsn == "" {
t.Skip("Test will not run without DEX_TEST_DSN environment variable.")
@ -88,7 +104,7 @@ func TestMigrateClientMetadata(t *testing.T) {
}
for i, tt := range tests {
model := &clientModel{
model := &oldClientModel{
ID: strconv.Itoa(i),
Secret: []byte("verysecret"),
Metadata: tt.before,
@ -108,12 +124,12 @@ func TestMigrateClientMetadata(t *testing.T) {
for i, tt := range tests {
id := strconv.Itoa(i)
m, err := dbMap.Get(clientModel{}, id)
m, err := dbMap.Get(oldClientModel{}, id)
if err != nil {
t.Errorf("case %d: failed to get model: %v", i, err)
continue
}
cim, ok := m.(*clientModel)
cim, ok := m.(*oldClientModel)
if !ok {
t.Errorf("case %d: unrecognized model type: %T", i, m)
continue

View file

@ -0,0 +1,4 @@
-- +migrate Up
ALTER TABLE client_identity ADD COLUMN "public" boolean;
UPDATE "client_identity" SET "public" = false;

View file

@ -78,6 +78,12 @@ var PostgresMigrations migrate.MigrationSource = &migrate.MemoryMigrationSource{
"-- +migrate Up\nCREATE TABLE IF NOT EXISTS \"trusted_peers\" (\n \"client_id\" text not null,\n \"trusted_client_id\" text not null,\n primary key (\"client_id\", \"trusted_client_id\")) ;\n",
},
},
{
Id: "0013_add_public_clients.sql",
Up: []string{
"-- +migrate Up\nALTER TABLE client_identity ADD COLUMN \"public\" boolean;\n\nUPDATE \"client_identity\" SET \"public\" = false;\n",
},
},
{
Id: "0013_add_scopes_to_refresh_tokens.sql",
Up: []string{

View file

@ -68,7 +68,7 @@ func (fi bindataFileInfo) Sys() interface{} {
return nil
}
var _dataIndexHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x7c\x91\xbd\x6e\x03\x21\x10\x84\x7b\x3f\xc5\x8a\x2a\x29\x2c\xfa\x08\x9f\x94\x3e\x55\x5e\x20\xe2\xb8\xb5\xbd\x12\x3f\x27\x58\xa2\xf8\xed\xb3\x88\x38\xc7\x45\x91\xbb\x19\xc1\x7e\x03\xb3\xe6\xca\xc1\x4f\x07\x00\x33\xa7\xe5\xd6\x84\xc8\x73\xca\x01\xac\x63\x4a\xf1\xa4\xb4\x4f\x17\x8a\xaa\x1f\xc9\x21\xdb\xd9\xe3\xdd\x35\x9f\x37\xd3\xec\x32\xc1\x6b\xe5\x2b\x46\x26\x67\x19\x41\x60\x2f\xc3\x85\x96\xb4\x9b\x00\x78\x72\x29\x04\x7b\x2c\xb8\xda\x2c\x13\x0b\x78\x2a\x0c\xe9\x0c\xce\x93\x60\x8e\xb4\x94\xe7\x31\x42\x4b\xc6\xdf\x48\x43\x71\xad\x0c\x7c\x5b\xf1\xa4\x18\xbf\x58\x41\xb4\x41\xb4\xcb\xa9\x94\x8f\x4e\x52\xd3\xcf\xf0\x61\x60\xfd\x3e\x46\x74\xff\xda\xdd\x8f\xc8\x52\xe7\x40\x02\xfd\xb4\xbe\x8a\x7d\x1b\x3a\x31\xba\xf5\xf5\x6f\x75\x19\x2f\xf2\x15\xcc\x5b\x7b\x0f\x98\xef\xfb\xcb\x1b\xd6\xe8\xbe\x1b\xa3\xfb\xb2\xbe\x03\x00\x00\xff\xff\x27\x69\xf8\xf2\xb4\x01\x00\x00")
var _dataIndexHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x94\x52\xcd\x4e\xc3\x30\x0c\xbe\xef\x29\xac\x9c\xe0\x30\x7a\x47\x6d\x25\x40\xdc\x90\x26\xf1\x02\x53\x9a\x78\x6d\xb4\xfc\x4c\x89\x8b\x36\x4d\x7b\x77\xdc\x96\xae\x5b\x81\x09\x6e\xfe\x14\xfb\xfb\x89\x9d\x37\xe4\x6c\xb9\x00\xc8\xab\xa0\x0f\xe5\x82\x2b\xae\x37\x21\x3a\x90\x8a\x4c\xf0\x85\xc8\x6c\xa8\x8d\x17\x65\xff\xc4\x8f\x24\x2b\x8b\x23\xea\x70\x9c\x40\x07\x75\x09\x4f\x2d\x35\xe8\xc9\x28\x49\x08\x4c\xf6\x78\xd1\xd0\x49\x5d\x4d\x00\xdc\xa9\xe0\x9c\x5c\x26\xdc\xc9\xc8\x13\x1a\xac\x49\x04\x61\x03\xca\x1a\xa6\x59\x1a\x9d\xee\x2f\x25\x32\xd6\x98\x4b\xe6\xc6\xef\x5a\x02\x3a\xec\xb0\x10\x84\x7b\x12\xe0\xa5\xe3\x5a\xc5\x90\xd2\x7a\x60\x12\x50\xce\xa6\x19\x9d\xcd\x70\x3d\x44\x3b\x1e\xc1\x6c\xe0\x61\xb5\x7a\x86\xd3\x69\x6a\xbd\x54\x48\x6d\xe5\x0c\xf3\x7d\x48\xdb\x32\x7c\xeb\xbf\xa8\x8b\xea\x48\xc6\x1a\xa9\x10\xeb\xca\x4a\xbf\x15\x3d\x1b\xda\x84\xff\xa4\x1a\xe6\xbc\x1e\xc7\xf2\xac\x23\xe7\x05\x7d\x37\x37\x5b\x97\x92\xd6\x56\x52\x6d\x05\x38\xa4\x26\xe8\x42\xb0\x9f\x8e\x70\xd0\x7e\x09\x1a\x17\x3f\xd8\xb8\xfa\x33\xee\x39\x1b\x9a\x36\x3f\xed\xed\x56\x80\xd7\xbd\x6a\xa4\xaf\xb1\x57\x1a\x75\x47\xfb\xd7\xa1\xbe\xc2\xf8\x40\xb7\x02\x45\xac\xf9\x1e\x30\x8a\xbf\xa8\xbf\x8f\xcd\x00\xd9\xef\xd2\x79\x36\x9c\x7b\x9e\x0d\xf7\xff\x19\x00\x00\xff\xff\xaf\x0b\xca\x75\x07\x03\x00\x00")
func dataIndexHtmlBytes() ([]byte, error) {
return bindataRead(
@ -83,7 +83,7 @@ func dataIndexHtml() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "data/index.html", size: 436, mode: os.FileMode(420), modTime: time.Unix(1465417812, 0)}
info := bindataFileInfo{name: "data/index.html", size: 775, mode: os.FileMode(420), modTime: time.Unix(1466378108, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}

View file

@ -1,5 +1,6 @@
<html>
<body>
<form action="/login">
<table>
<tr>
@ -7,15 +8,29 @@
<br>
(comma-separated list of client-ids)
</td>
<td> <input type="text" name="cross_client"> </td>
<td> <input type="text" name="cross_client" > </td>
</tr>
</table>
{{ if .OOB }}
<input type="submit" value="Login" formtarget="_blank">
{{ else }}
<input type="submit" value="Login" >
{{ end }}
</form>
<input type="submit" value="Login">
{{ if .OOB }}
<form action="/callback" method="get" >
Code
<input type="text" name="code" value="">
<br>
<input type="submit" value="Exchange Code" >
</form>
{{ end }}
{{ if not .OOB }}
<form action="/register">
<input type="submit" value="Register">
<input type="submit" value="Register" />
</form>
{{ end }}
</body>
</html>

View file

@ -23,6 +23,7 @@ import (
"github.com/coreos/go-oidc/oauth2"
"github.com/coreos/go-oidc/oidc"
"github.com/coreos/dex/client"
pflag "github.com/coreos/dex/pkg/flag"
phttp "github.com/coreos/dex/pkg/http"
"github.com/coreos/dex/pkg/log"
@ -163,15 +164,21 @@ func main() {
func NewClientHandler(c *oidc.Client, issuer string, cbURL url.URL) http.Handler {
mux := http.NewServeMux()
oob := cbURL.String() == client.OOBRedirectURI
issuerURL, err := url.Parse(issuer)
if err != nil {
log.Fatalf("Could not parse issuer url: %v", err)
}
mux.HandleFunc("/", handleIndex)
mux.HandleFunc("/", handleIndexFunc(oob))
mux.HandleFunc("/login", handleLoginFunc(c))
mux.HandleFunc("/register", handleRegisterFunc(c))
mux.HandleFunc(cbURL.Path, handleCallbackFunc(c))
if cbURL.String() != client.OOBRedirectURI {
mux.HandleFunc(cbURL.Path, handleCallbackFunc(c))
} else {
mux.HandleFunc("/callback", handleCallbackFunc(c))
}
resendURL := *issuerURL
resendURL.Path = "/resend-verify-email"
@ -180,12 +187,16 @@ func NewClientHandler(c *oidc.Client, issuer string, cbURL url.URL) http.Handler
return mux
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
err := indexTemplate.Execute(w, nil)
if err != nil {
phttp.WriteError(w, http.StatusInternalServerError,
fmt.Sprintf("unable to execute template: %v", err))
func handleIndexFunc(oob bool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := indexTemplate.Execute(w, map[string]interface{}{
"OOB": oob,
})
if err != nil {
phttp.WriteError(w, http.StatusInternalServerError,
fmt.Sprintf("unable to execute template: %v", err))
}
}
}

View file

@ -383,7 +383,11 @@ func TestCreateClient(t *testing.T) {
}
addIDAndSecret := func(cli adminschema.Client) *adminschema.Client {
cli.Id = "client_auth.example.com"
if cli.Public {
cli.Id = "client_" + cli.ClientName
} else {
cli.Id = "client_auth.example.com"
}
cli.Secret = base64.URLEncoding.EncodeToString([]byte("client_0"))
return &cli
}
@ -400,6 +404,23 @@ func TestCreateClient(t *testing.T) {
},
}
clientPublicGood := clientGood
clientPublicGood.Public = true
clientPublicGood.Metadata.ClientName = "PublicName"
clientPublicGood.Metadata.RedirectURIs = []url.URL{}
clientPublicGood.Credentials.ID = "client_PublicName"
adminPublicClientGood := adminClientGood
adminPublicClientGood.Public = true
adminPublicClientGood.ClientName = "PublicName"
adminPublicClientGood.RedirectURIs = []string{}
adminPublicClientMissingName := adminPublicClientGood
adminPublicClientMissingName.ClientName = ""
adminPublicClientHasARedirect := adminPublicClientGood
adminPublicClientHasARedirect.RedirectURIs = []string{"https://auth.example.com/"}
adminAdminClient := adminClientGood
adminAdminClient.IsAdmin = true
clientGoodAdmin := clientGood
@ -479,6 +500,27 @@ func TestCreateClient(t *testing.T) {
wantClient: clientGood,
wantTrustedPeers: []string{"test_client_0"},
},
{
req: adminschema.ClientCreateRequest{
Client: &adminPublicClientGood,
},
want: adminschema.ClientCreateResponse{
Client: addIDAndSecret(adminPublicClientGood),
},
wantClient: clientPublicGood,
},
{
req: adminschema.ClientCreateRequest{
Client: &adminPublicClientMissingName,
},
wantError: http.StatusBadRequest,
},
{
req: adminschema.ClientCreateRequest{
Client: &adminPublicClientHasARedirect,
},
wantError: http.StatusBadRequest,
},
}
for i, tt := range tests {
@ -530,6 +572,7 @@ func TestCreateClient(t *testing.T) {
repoClient, err := f.cr.Get(nil, resp.Client.Id)
if err != nil {
t.Errorf("case %d: Unexpected error getting client: %v", i, err)
continue
}
if diff := pretty.Compare(tt.wantClient, repoClient); diff != "" {

View file

@ -303,30 +303,69 @@ func TestHTTPClientCredsToken(t *testing.T) {
},
},
}
cis := []client.LoadableClient{{Client: ci}}
srv, err := mockServer(cis)
if err != nil {
t.Fatalf("Unexpected error setting up server: %v", err)
ci2 := ci
ci2.Credentials.ID = "not_a_client"
ciPublic := ci
ciPublic.Public = true
ciPublic.Credentials.ID = "public"
cis := []client.LoadableClient{{Client: ci}, {Client: ciPublic}}
tests := []struct {
cli client.Client
clients []client.LoadableClient
wantErr bool
}{
{
cli: ci,
clients: cis,
wantErr: false,
},
{
cli: ci2,
clients: cis,
wantErr: true,
},
{
cli: ciPublic,
clients: cis,
wantErr: true,
},
}
cl, err := mockClient(srv, ci)
if err != nil {
t.Fatalf("Unexpected error setting up OIDC client: %v", err)
}
for i, tt := range tests {
srv, err := mockServer(tt.clients)
if err != nil {
t.Fatalf("case %d: Unexpected error setting up server: %v", i, err)
}
tok, err := cl.ClientCredsToken([]string{"openid"})
if err != nil {
t.Fatalf("Failed getting client token: %v", err)
}
cl, err := mockClient(srv, tt.cli)
if err != nil {
t.Fatalf("case %d: Unexpected error setting up OIDC client: %v", i, err)
}
claims, err := tok.Claims()
if err != nil {
t.Fatalf("Failed parsing claims from client token: %v", err)
}
tok, err := cl.ClientCredsToken([]string{"openid"})
if tt.wantErr {
if err == nil {
t.Errorf("case %d: want non-nil error", i)
}
continue
}
if err := verifyUserClaims(claims, &ci, nil, srv.IssuerURL); err != nil {
t.Fatalf("Failed to verify claims: %v", err)
if err != nil {
t.Fatalf("case %d: Failed getting client token: %v", i, err)
continue
}
claims, err := tok.Claims()
if err != nil {
t.Fatalf("case %d: Failed parsing claims from client token: %v", i, err)
}
if err := verifyUserClaims(claims, &ci, nil, srv.IssuerURL); err != nil {
t.Fatalf("case %d: Failed to verify claims: %v", i, err)
}
}
}

View file

@ -26,11 +26,12 @@ __Version:__ v1
```
{
clientName: string // OPTIONAL. Name of the Client to be presented to the End-User. If desired, representation of this Claim in different languages and scripts is represented as described in Section 2.1 ( Metadata Languages and Scripts ) .,
clientName: string // OPTIONAL for normal cliens. Name of the Client to be presented to the End-User. If desired, representation of this Claim in different languages and scripts is represented as described in Section 2.1 ( Metadata Languages and Scripts ). REQUIRED for public clients,
clientURI: string // OPTIONAL. URL of the home page of the Client. The value of this field MUST point to a valid Web page. If present, the server SHOULD display this URL to the End-User in a followable fashion. If desired, representation of this Claim in different languages and scripts is represented as described in Section 2.1 ( Metadata Languages and Scripts ) .,
id: string // The client ID. Ignored in client create requests.,
isAdmin: boolean,
logoURI: string // OPTIONAL. URL that references a logo for the Client application. If present, the server SHOULD display this image to the End-User during approval. The value of this field MUST point to a valid image file. If desired, representation of this Claim in different languages and scripts is represented as described in Section 2.1 ( Metadata Languages and Scripts ) .,
public: boolean // OPTIONAL. Determines if the client is public. Public clients have certain restrictions: They cannot use their credentials to obtain a client JWT. Their redirects URLs cannot be specified: they are always http://localhost:$PORT or urn:ietf:wg:oauth:2.0:oob,
redirectURIs: [
string
],

View file

@ -24,6 +24,7 @@ func MapSchemaClientToClient(sc Client) (client.Client, error) {
Metadata: oidc.ClientMetadata{
RedirectURIs: make([]url.URL, len(sc.RedirectURIs)),
},
Public: sc.Public,
}
for i, ru := range sc.RedirectURIs {
if ru == "" {
@ -65,6 +66,8 @@ func MapClientToSchemaClient(c client.Client) Client {
Id: c.Credentials.ID,
Secret: c.Credentials.Secret,
RedirectURIs: make([]string, len(c.Metadata.RedirectURIs)),
IsAdmin: c.Admin,
Public: c.Public,
}
for i, u := range c.Metadata.RedirectURIs {
cl.RedirectURIs[i] = u.String()
@ -78,6 +81,5 @@ func MapClientToSchemaClient(c client.Client) Client {
if c.Metadata.ClientURI != nil {
cl.ClientURI = c.Metadata.ClientURI.String()
}
cl.IsAdmin = c.Admin
return cl
}

View file

@ -43,6 +43,23 @@ func TestMapSchemaClientToClient(t *testing.T) {
ClientURI: mustParseURL(t, "https://clientURI.example.com"),
},
},
}, {
sc: Client{
Id: "456",
Secret: "sec_456",
ClientName: "Dave",
Public: true,
},
want: client.Client{
Credentials: oidc.ClientCredentials{
ID: "456",
Secret: "sec_456",
},
Metadata: oidc.ClientMetadata{
ClientName: "Dave",
},
Public: true,
},
}, {
sc: Client{
Id: "123",
@ -108,6 +125,24 @@ func TestMapClientToClientSchema(t *testing.T) {
},
},
},
{
want: Client{
Id: "456",
Secret: "sec_456",
ClientName: "Dave",
Public: true,
},
c: client.Client{
Credentials: oidc.ClientCredentials{
ID: "456",
Secret: "sec_456",
},
Metadata: oidc.ClientMetadata{
ClientName: "Dave",
},
Public: true,
},
},
}
for i, tt := range tests {

View file

@ -110,10 +110,11 @@ type Admin struct {
}
type Client struct {
// ClientName: OPTIONAL. Name of the Client to be presented to the
// End-User. If desired, representation of this Claim in different
// languages and scripts is represented as described in Section 2.1 (
// Metadata Languages and Scripts ) .
// ClientName: OPTIONAL for normal cliens. Name of the Client to be
// presented to the End-User. If desired, representation of this Claim
// in different languages and scripts is represented as described in
// Section 2.1 ( Metadata Languages and Scripts ). REQUIRED for public
// clients
ClientName string `json:"clientName,omitempty"`
// ClientURI: OPTIONAL. URL of the home page of the Client. The value of
@ -137,13 +138,20 @@ type Client struct {
// Section 2.1 ( Metadata Languages and Scripts ) .
LogoURI string `json:"logoURI,omitempty"`
// RedirectURIs: REQUIRED. Array of Redirection URI values used by the
// Client. One of these registered Redirection URI values MUST exactly
// match the redirect_uri parameter value used in each Authorization
// Request, with the matching performed as described in Section 6.2.1 of
// [RFC3986] ( Berners-Lee, T., Fielding, R., and L. Masinter,
// “Uniform Resource Identifier (URI): Generic Syntax,” January
// 2005. ) (Simple String Comparison).
// Public: OPTIONAL. Determines if the client is public. Public clients
// have certain restrictions: They cannot use their credentials to
// obtain a client JWT. Their redirects URLs cannot be specified: they
// are always http://localhost:$PORT or urn:ietf:wg:oauth:2.0:oob
Public bool `json:"public,omitempty"`
// RedirectURIs: REQUIRED for normal clients. Array of Redirection URI
// values used by the Client. One of these registered Redirection URI
// values MUST exactly match the redirect_uri parameter value used in
// each Authorization Request, with the matching performed as described
// in Section 6.2.1 of [RFC3986] ( Berners-Lee, T., Fielding, R., and L.
// Masinter, “Uniform Resource Identifier (URI): Generic Syntax,”
// January 2005. ) (Simple String Comparison). DISALLOWED for public
// clients.
RedirectURIs []string `json:"redirectURIs,omitempty"`
// Secret: The client secret. Ignored in client create requests.

View file

@ -72,11 +72,11 @@ const DiscoveryJSON = `{
"items": {
"type": "string"
},
"description": "REQUIRED. Array of Redirection URI values used by the Client. One of these registered Redirection URI values MUST exactly match the redirect_uri parameter value used in each Authorization Request, with the matching performed as described in Section 6.2.1 of [RFC3986] ( Berners-Lee, T., Fielding, R., and L. Masinter, “Uniform Resource Identifier (URI): Generic Syntax,” January 2005. ) (Simple String Comparison)."
"description": "REQUIRED for normal clients. Array of Redirection URI values used by the Client. One of these registered Redirection URI values MUST exactly match the redirect_uri parameter value used in each Authorization Request, with the matching performed as described in Section 6.2.1 of [RFC3986] ( Berners-Lee, T., Fielding, R., and L. Masinter, “Uniform Resource Identifier (URI): Generic Syntax,” January 2005. ) (Simple String Comparison). DISALLOWED for public clients."
},
"clientName": {
"type": "string",
"description": "OPTIONAL. Name of the Client to be presented to the End-User. If desired, representation of this Claim in different languages and scripts is represented as described in Section 2.1 ( Metadata Languages and Scripts ) ."
"description": "OPTIONAL for normal cliens. Name of the Client to be presented to the End-User. If desired, representation of this Claim in different languages and scripts is represented as described in Section 2.1 ( Metadata Languages and Scripts ). REQUIRED for public clients"
},
"logoURI": {
"type": "string",
@ -92,6 +92,10 @@ const DiscoveryJSON = `{
"type": "string"
},
"description": "Array of ClientIDs of clients that are allowed to mint ID tokens for the client being created."
},
"public": {
"type": "boolean",
"description": "OPTIONAL. Determines if the client is public. Public clients have certain restrictions: They cannot use their credentials to obtain a client JWT. Their redirects URLs cannot be specified: they are always http://localhost:$PORT or urn:ietf:wg:oauth:2.0:oob"
}
}
},

View file

@ -65,11 +65,11 @@
"items": {
"type": "string"
},
"description": "REQUIRED. Array of Redirection URI values used by the Client. One of these registered Redirection URI values MUST exactly match the redirect_uri parameter value used in each Authorization Request, with the matching performed as described in Section 6.2.1 of [RFC3986] ( Berners-Lee, T., Fielding, R., and L. Masinter, “Uniform Resource Identifier (URI): Generic Syntax,” January 2005. ) (Simple String Comparison)."
"description": "REQUIRED for normal clients. Array of Redirection URI values used by the Client. One of these registered Redirection URI values MUST exactly match the redirect_uri parameter value used in each Authorization Request, with the matching performed as described in Section 6.2.1 of [RFC3986] ( Berners-Lee, T., Fielding, R., and L. Masinter, “Uniform Resource Identifier (URI): Generic Syntax,” January 2005. ) (Simple String Comparison). DISALLOWED for public clients."
},
"clientName": {
"type": "string",
"description": "OPTIONAL. Name of the Client to be presented to the End-User. If desired, representation of this Claim in different languages and scripts is represented as described in Section 2.1 ( Metadata Languages and Scripts ) ."
"description": "OPTIONAL for normal cliens. Name of the Client to be presented to the End-User. If desired, representation of this Claim in different languages and scripts is represented as described in Section 2.1 ( Metadata Languages and Scripts ). REQUIRED for public clients"
},
"logoURI": {
"type": "string",
@ -85,6 +85,10 @@
"type": "string"
},
"description": "Array of ClientIDs of clients that are allowed to mint ID tokens for the client being created."
},
"public": {
"type": "boolean",
"description": "OPTIONAL. Determines if the client is public. Public clients have certain restrictions: They cannot use their credentials to obtain a client JWT. Their redirects URLs cannot be specified: they are always http://localhost:$PORT or urn:ietf:wg:oauth:2.0:oob."
}
}
},

View file

@ -299,35 +299,23 @@ func getTemplates(issuerName, issuerLogoURL string,
}
func setTemplates(srv *Server, tpls *template.Template) error {
ltpl, err := findTemplate(LoginPageTemplateName, tpls)
if err != nil {
return err
for _, t := range []struct {
templateName string
templatePtr **template.Template
}{
{LoginPageTemplateName, &srv.LoginTemplate},
{RegisterTemplateName, &srv.RegisterTemplate},
{VerifyEmailTemplateName, &srv.VerifyEmailTemplate},
{SendResetPasswordEmailTemplateName, &srv.SendResetPasswordEmailTemplate},
{ResetPasswordTemplateName, &srv.ResetPasswordTemplate},
{OOBTemplateName, &srv.OOBTemplate},
} {
tpl, err := findTemplate(t.templateName, tpls)
if err != nil {
return err
}
*t.templatePtr = tpl
}
srv.LoginTemplate = ltpl
rtpl, err := findTemplate(RegisterTemplateName, tpls)
if err != nil {
return err
}
srv.RegisterTemplate = rtpl
vtpl, err := findTemplate(VerifyEmailTemplateName, tpls)
if err != nil {
return err
}
srv.VerifyEmailTemplate = vtpl
srtpl, err := findTemplate(SendResetPasswordEmailTemplateName, tpls)
if err != nil {
return err
}
srv.SendResetPasswordEmailTemplate = srtpl
rpwtpl, err := findTemplate(ResetPasswordTemplateName, tpls)
if err != nil {
return err
}
srv.ResetPasswordTemplate = rpwtpl
return nil
}

View file

@ -44,6 +44,7 @@ var (
httpPathAcceptInvitation = "/accept-invitation"
httpPathDebugVars = "/debug/vars"
httpPathClientRegistration = "/registration"
httpPathOOB = "/oob"
cookieLastSeen = "LastSeen"
cookieShowEmailVerifiedMessage = "ShowEmailVerifiedMessage"
@ -188,7 +189,7 @@ func renderLoginPage(w http.ResponseWriter, r *http.Request, srv OIDCServer, idp
// Render error message if client id is invalid.
clientID := q.Get("client_id")
cm, err := srv.ClientMetadata(clientID)
_, err := srv.Client(clientID)
if err != nil {
log.Errorf("Failed fetching client %q from repo: %v", clientID, err)
td.Error = true
@ -196,7 +197,7 @@ func renderLoginPage(w http.ResponseWriter, r *http.Request, srv OIDCServer, idp
execTemplate(w, tpl, td)
return
}
if cm == nil {
if err == client.ErrorNotFound {
td.Error = true
td.Message = "Authentication Error"
td.Detail = "Invalid client ID"
@ -299,25 +300,19 @@ func handleAuthFunc(srv OIDCServer, idpcs []connector.Connector, tpl *template.T
return
}
cm, err := srv.ClientMetadata(acr.ClientID)
cli, err := srv.Client(acr.ClientID)
if err != nil {
log.Errorf("Failed fetching client %q from repo: %v", acr.ClientID, err)
writeAuthError(w, oauth2.NewError(oauth2.ErrorServerError), acr.State)
return
}
if cm == nil {
if err == client.ErrorNotFound {
log.Errorf("Client %q not found", acr.ClientID)
writeAuthError(w, oauth2.NewError(oauth2.ErrorInvalidRequest), acr.State)
return
}
if len(cm.RedirectURIs) == 0 {
log.Errorf("Client %q has no redirect URLs", acr.ClientID)
writeAuthError(w, oauth2.NewError(oauth2.ErrorServerError), acr.State)
return
}
redirectURL, err := client.ValidRedirectURL(acr.RedirectURL, cm.RedirectURIs)
redirectURL, err := cli.ValidRedirectURL(acr.RedirectURL)
if err != nil {
switch err {
case (client.ErrorCantChooseRedirectURL):
@ -554,6 +549,37 @@ func handleTokenFunc(srv OIDCServer) http.HandlerFunc {
}
}
func handleOOBFunc(s *Server, tpl *template.Template) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
w.Header().Set("Allow", "GET")
phttp.WriteError(w, http.StatusMethodNotAllowed, "GET only acceptable method")
return
}
key := r.URL.Query().Get("code")
if key == "" {
phttp.WriteError(w, http.StatusBadRequest, "Invalid Session")
return
}
sessionID, err := s.SessionManager.ExchangeKey(key)
if err != nil {
phttp.WriteError(w, http.StatusBadRequest, "Invalid Session")
return
}
code, err := s.SessionManager.NewSessionKey(sessionID)
if err != nil {
log.Errorf("problem getting NewSessionKey: %v", err)
phttp.WriteError(w, http.StatusInternalServerError, "Internal Server Error")
return
}
execTemplate(w, tpl, map[string]string{
"code": code,
})
}
}
func makeHealthHandler(checks []health.Checkable) http.Handler {
return health.Checker{
Checks: checks,

View file

@ -105,6 +105,30 @@ func TestHandleAuthFuncResponsesSingleRedirectURL(t *testing.T) {
wantLocation: "http://fake.example.com",
},
// valid redirect_uri for public client
{
query: url.Values{
"response_type": []string{"code"},
"redirect_uri": []string{"http://localhost:8080"},
"client_id": []string{testPublicClientID},
"connector_id": []string{"fake"},
"scope": []string{"openid"},
},
wantCode: http.StatusFound,
wantLocation: "http://fake.example.com",
},
// valid OOB redirect_uri for public client
{
query: url.Values{
"response_type": []string{"code"},
"redirect_uri": []string{client.OOBRedirectURI},
"client_id": []string{testPublicClientID},
"connector_id": []string{"fake"},
"scope": []string{"openid"},
},
wantCode: http.StatusFound,
wantLocation: "http://fake.example.com",
},
// provided redirect_uri does not match client
{
query: url.Values{
@ -173,6 +197,17 @@ func TestHandleAuthFuncResponsesSingleRedirectURL(t *testing.T) {
},
wantCode: http.StatusBadRequest,
},
// invalid redirect_uri for public client
{
query: url.Values{
"response_type": []string{"code"},
"redirect_uri": []string{client.OOBRedirectURI + "oops"},
"client_id": []string{testPublicClientID},
"connector_id": []string{"fake"},
"scope": []string{"openid"},
},
wantCode: http.StatusBadRequest,
},
}
for i, tt := range tests {

View file

@ -38,12 +38,12 @@ const (
VerifyEmailTemplateName = "verify-email.html"
SendResetPasswordEmailTemplateName = "send-reset-password.html"
ResetPasswordTemplateName = "reset-password.html"
APIVersion = "v1"
OOBTemplateName = "oob-template.html"
APIVersion = "v1"
)
type OIDCServer interface {
ClientMetadata(string) (*oidc.ClientMetadata, error)
Client(string) (client.Client, error)
NewSession(connectorID, clientID, clientState string, redirectURL url.URL, nonce string, register bool, scope []string) (string, error)
Login(oidc.Identity, string) (string, error)
@ -72,6 +72,7 @@ type Server struct {
VerifyEmailTemplate *template.Template
SendResetPasswordEmailTemplate *template.Template
ResetPasswordTemplate *template.Template
OOBTemplate *template.Template
HealthChecks []health.Checkable
Connectors []connector.Connector
@ -214,6 +215,7 @@ func (s *Server) HTTPHandler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc(httpPathDiscovery, handleDiscoveryFunc(s.ProviderConfig()))
mux.HandleFunc(httpPathAuth, handleAuthFunc(s, s.Connectors, s.LoginTemplate, s.EnableRegistration))
mux.HandleFunc(httpPathOOB, handleOOBFunc(s, s.OOBTemplate))
mux.HandleFunc(httpPathToken, handleTokenFunc(s))
mux.HandleFunc(httpPathKeys, handleKeysFunc(s.KeyManager, clock))
mux.Handle(httpPathHealth, makeHealthHandler(checks))
@ -290,8 +292,8 @@ func (s *Server) NewClientTokenAuthHandler(handler http.Handler) http.Handler {
}
}
func (s *Server) ClientMetadata(clientID string) (*oidc.ClientMetadata, error) {
return s.ClientManager.Metadata(clientID)
func (s *Server) Client(clientID string) (client.Client, error) {
return s.ClientManager.Get(clientID)
}
func (s *Server) NewSession(ipdcID, clientID, clientState string, redirectURL url.URL, nonce string, register bool, scope []string) (string, error) {
@ -399,6 +401,9 @@ func (s *Server) Login(ident oidc.Identity, key string) (string, error) {
}
ru := ses.RedirectURL
if ru.String() == client.OOBRedirectURI {
ru = s.absURL(httpPathOOB)
}
q := ru.Query()
q.Set("code", code)
q.Set("state", ses.ClientState)
@ -408,6 +413,15 @@ func (s *Server) Login(ident oidc.Identity, key string) (string, error) {
}
func (s *Server) ClientCredsToken(creds oidc.ClientCredentials) (*jose.JWT, error) {
cli, err := s.Client(creds.ID)
if err != nil {
return nil, err
}
if cli.Public {
return nil, oauth2.NewError(oauth2.ErrorInvalidClient)
}
ok, err := s.ClientManager.Authenticate(creds)
if err != nil {
log.Errorf("Failed fetching client %s from manager: %v", creds.ID, err)

View file

@ -39,6 +39,13 @@ var (
ID: testClientID,
Secret: clientTestSecret,
}
testPublicClientID = "publicclient.example.com"
publicClientTestSecret = base64.URLEncoding.EncodeToString([]byte("secret"))
testPublicClientCredentials = oidc.ClientCredentials{
ID: testPublicClientID,
Secret: publicClientTestSecret,
}
testClients = []client.LoadableClient{
{
Client: client.Client{
@ -50,6 +57,12 @@ var (
},
},
},
{
Client: client.Client{
Credentials: testPublicClientCredentials,
Public: true,
},
},
}
testConnectorID1 = "IDPC-1"

View file

@ -3,7 +3,7 @@
"id": "XXX",
"secret": "c2VjcmV0ZQ==",
"redirectURLs": ["http://127.0.0.1:5555/callback"],
"trustedPeers": ["example-app"]
"trustedPeers": ["example-app", "public"]
},
{
"id": "example-app",
@ -15,6 +15,11 @@
"secret": "ZXhhbXBsZS1jbGktc2VjcmV0",
"redirectURLs": ["http://127.0.0.1:8000/admin/v1/oauth/login"]
},
{
"id": "public",
"secret": "ZXhhbXBsZS1hcHAtc2VjcmV0",
"public": true
},
{
"id": "oauth2_proxy",
"secret": "cHJveHk=",

View file

@ -0,0 +1,11 @@
{{ template "header.html" }}
<div class="panel">
<h2 class="heading">Login Successful</h2>
Please copy this code, switch to your application and paste it there:
<br/>
<input type="text" value="{{ .code }}" />
</div>
{{ template "footer.html" }}