forked from mystiq/dex
Merge pull request #471 from bobbyrullo/native
Implement Public Clients
This commit is contained in:
commit
3b8d704c9c
29 changed files with 763 additions and 116 deletions
58
Documentation/clients.md
Normal file
58
Documentation/clients.md
Normal 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.
|
20
admin/api.go
20
admin/api.go
|
@ -69,10 +69,15 @@ func errorMaker(typ string, desc string, code int) func(internal error) Error {
|
||||||
var (
|
var (
|
||||||
ErrorMissingClient = errorMaker("bad_request", "The 'client' cannot be empty", http.StatusBadRequest)(nil)
|
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)
|
ErrorInvalidClientFunc = errorMaker("bad_request", "Your client could not be validated.", http.StatusBadRequest)
|
||||||
|
|
||||||
errorMap = map[error]func(error) Error{
|
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.ErrorNotFound: errorMaker("resource_not_found", "Resource could not be found.", http.StatusNotFound),
|
||||||
user.ErrorDuplicateEmail: errorMaker("bad_request", "Email already in use.", http.StatusBadRequest),
|
user.ErrorDuplicateEmail: errorMaker("bad_request", "Email already in use.", http.StatusBadRequest),
|
||||||
user.ErrorInvalidEmail: errorMaker("bad_request", "invalid email.", 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) {
|
func (a *AdminAPI) GetAdmin(id string) (adminschema.Admin, error) {
|
||||||
usr, err := a.userRepo.Get(nil, id)
|
usr, err := a.userRepo.Get(nil, id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return adminschema.Admin{}, mapError(err)
|
return adminschema.Admin{}, mapError(err)
|
||||||
}
|
}
|
||||||
|
@ -136,15 +140,9 @@ func (a *AdminAPI) CreateClient(req adminschema.ClientCreateRequest) (adminschem
|
||||||
return adminschema.ClientCreateResponse{}, mapError(err)
|
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{
|
creds, err := a.clientManager.New(cli, &clientmanager.ClientOptions{
|
||||||
TrustedPeers: req.Client.TrustedPeers,
|
TrustedPeers: req.Client.TrustedPeers,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return adminschema.ClientCreateResponse{}, mapError(err)
|
return adminschema.ClientCreateResponse{}, mapError(err)
|
||||||
}
|
}
|
||||||
|
@ -165,6 +163,12 @@ func (a *AdminAPI) GetConnectors() ([]connector.ConnectorConfig, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapError(e error) error {
|
func mapError(e error) error {
|
||||||
|
switch t := e.(type) {
|
||||||
|
case client.ValidationError:
|
||||||
|
return ErrorInvalidClientFunc(t)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
if mapped, ok := errorMap[e]; ok {
|
if mapped, ok := errorMap[e]; ok {
|
||||||
return mapped(e)
|
return mapped(e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
@ -19,11 +20,27 @@ var (
|
||||||
ErrorInvalidRedirectURL = errors.New("not a valid redirect url for the given client")
|
ErrorInvalidRedirectURL = errors.New("not a valid redirect url for the given client")
|
||||||
ErrorCantChooseRedirectURL = errors.New("must provide a redirect url; client has many")
|
ErrorCantChooseRedirectURL = errors.New("must provide a redirect url; client has many")
|
||||||
ErrorNoValidRedirectURLs = errors.New("no valid redirect URLs for this client.")
|
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 (
|
const (
|
||||||
bcryptHashCost = 10
|
bcryptHashCost = 10
|
||||||
|
|
||||||
|
OOBRedirectURI = "urn:ietf:wg:oauth:2.0:oob"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HashSecret(creds oidc.ClientCredentials) ([]byte, error) {
|
func HashSecret(creds oidc.ClientCredentials) ([]byte, error) {
|
||||||
|
@ -44,6 +61,35 @@ type Client struct {
|
||||||
Credentials oidc.ClientCredentials
|
Credentials oidc.ClientCredentials
|
||||||
Metadata oidc.ClientMetadata
|
Metadata oidc.ClientMetadata
|
||||||
Admin bool
|
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 {
|
type ClientRepo interface {
|
||||||
|
@ -106,6 +152,7 @@ func ClientsFromReader(r io.Reader) ([]LoadableClient, error) {
|
||||||
Secret string `json:"secret"`
|
Secret string `json:"secret"`
|
||||||
RedirectURLs []string `json:"redirectURLs"`
|
RedirectURLs []string `json:"redirectURLs"`
|
||||||
Admin bool `json:"admin"`
|
Admin bool `json:"admin"`
|
||||||
|
Public bool `json:"public"`
|
||||||
TrustedPeers []string `json:"trustedPeers"`
|
TrustedPeers []string `json:"trustedPeers"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r).Decode(&c); err != nil {
|
if err := json.NewDecoder(r).Decode(&c); err != nil {
|
||||||
|
@ -137,7 +184,8 @@ func ClientsFromReader(r io.Reader) ([]LoadableClient, error) {
|
||||||
Metadata: oidc.ClientMetadata{
|
Metadata: oidc.ClientMetadata{
|
||||||
RedirectURIs: redirectURIs,
|
RedirectURIs: redirectURIs,
|
||||||
},
|
},
|
||||||
Admin: client.Admin,
|
Admin: client.Admin,
|
||||||
|
Public: client.Public,
|
||||||
},
|
},
|
||||||
TrustedPeers: client.TrustedPeers,
|
TrustedPeers: client.TrustedPeers,
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,13 @@ var (
|
||||||
"trustedPeers":["goodClient1", "goodClient2"]
|
"trustedPeers":["goodClient1", "goodClient2"]
|
||||||
}`
|
}`
|
||||||
|
|
||||||
|
publicClient = `{
|
||||||
|
"id": "public_client",
|
||||||
|
"secret": "` + goodSecret3 + `",
|
||||||
|
"redirectURLs": ["http://localhost:8080","urn:ietf:wg:oauth:2.0:oob"],
|
||||||
|
"public": true
|
||||||
|
}`
|
||||||
|
|
||||||
badURLClient = `{
|
badURLClient = `{
|
||||||
"id": "my_id",
|
"id": "my_id",
|
||||||
"secret": "` + goodSecret1 + `",
|
"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 + "]",
|
json: "[" + badURLClient + "]",
|
||||||
wantErr: true,
|
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 {
|
func mustParseURL(t *testing.T, s string) url.URL {
|
||||||
u, err := url.Parse(s)
|
u, err := url.Parse(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
@ -21,6 +22,10 @@ const (
|
||||||
maxSecretLength = 72
|
maxSecretLength = 72
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
localHostRedirectURL = mustParseURL("http://localhost:0")
|
||||||
|
)
|
||||||
|
|
||||||
type ClientOptions struct {
|
type ClientOptions struct {
|
||||||
TrustedPeers []string
|
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) {
|
func (m *ClientManager) New(cli client.Client, options *ClientOptions) (*oidc.ClientCredentials, error) {
|
||||||
tx, err := m.begin()
|
tx, err := m.begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -74,11 +83,14 @@ func (m *ClientManager) New(cli client.Client, options *ClientOptions) (*oidc.Cl
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
if err := validateClient(cli); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
err = m.addClientCredentials(&cli)
|
err = m.addClientCredentials(&cli)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
creds := cli.Credentials
|
creds := cli.Credentials
|
||||||
|
|
||||||
// Save Client
|
// Save Client
|
||||||
|
@ -156,7 +168,13 @@ func (m *ClientManager) SetDexAdmin(clientID string, isAdmin bool) error {
|
||||||
|
|
||||||
func (m *ClientManager) Authenticate(creds oidc.ClientCredentials) (bool, error) {
|
func (m *ClientManager) Authenticate(creds oidc.ClientCredentials) (bool, error) {
|
||||||
clientSecret, err := m.clientRepo.GetSecret(nil, creds.ID)
|
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
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,11 +189,15 @@ func (m *ClientManager) Authenticate(creds oidc.ClientCredentials) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ClientManager) addClientCredentials(cli *client.Client) error {
|
func (m *ClientManager) addClientCredentials(cli *client.Client) error {
|
||||||
// Generate Client ID
|
var seed string
|
||||||
if len(cli.Metadata.RedirectURIs) < 1 {
|
if cli.Public {
|
||||||
return errors.New("no client redirect url given")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -192,3 +214,37 @@ func (m *ClientManager) addClientCredentials(cli *client.Client) error {
|
||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
33
db/client.go
33
db/client.go
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/oidc"
|
"github.com/coreos/go-oidc/oidc"
|
||||||
|
@ -23,6 +24,10 @@ const (
|
||||||
pgErrorCodeUniqueViolation = "23505" // unique_violation
|
pgErrorCodeUniqueViolation = "23505" // unique_violation
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
localHostRedirectURL = mustParseURL("http://localhost:0")
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
register(table{
|
register(table{
|
||||||
name: clientTableName,
|
name: clientTableName,
|
||||||
|
@ -44,6 +49,16 @@ func newClientModel(cli client.Client) (*clientModel, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
bmeta, err := json.Marshal(&cli.Metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -54,6 +69,7 @@ func newClientModel(cli client.Client) (*clientModel, error) {
|
||||||
Secret: hashed,
|
Secret: hashed,
|
||||||
Metadata: string(bmeta),
|
Metadata: string(bmeta),
|
||||||
DexAdmin: cli.Admin,
|
DexAdmin: cli.Admin,
|
||||||
|
Public: cli.Public,
|
||||||
}
|
}
|
||||||
|
|
||||||
return &cim, nil
|
return &cim, nil
|
||||||
|
@ -64,6 +80,7 @@ type clientModel struct {
|
||||||
Secret []byte `db:"secret"`
|
Secret []byte `db:"secret"`
|
||||||
Metadata string `db:"metadata"`
|
Metadata string `db:"metadata"`
|
||||||
DexAdmin bool `db:"dex_admin"`
|
DexAdmin bool `db:"dex_admin"`
|
||||||
|
Public bool `db:"public"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type trustedPeerModel struct {
|
type trustedPeerModel struct {
|
||||||
|
@ -76,13 +93,18 @@ func (m *clientModel) Client() (*client.Client, error) {
|
||||||
Credentials: oidc.ClientCredentials{
|
Credentials: oidc.ClientCredentials{
|
||||||
ID: m.ID,
|
ID: m.ID,
|
||||||
},
|
},
|
||||||
Admin: m.DexAdmin,
|
Admin: m.DexAdmin,
|
||||||
|
Public: m.Public,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal([]byte(m.Metadata), &ci.Metadata); err != nil {
|
if err := json.Unmarshal([]byte(m.Metadata), &ci.Metadata); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ci.Public {
|
||||||
|
ci.Metadata.RedirectURIs = []url.URL{}
|
||||||
|
}
|
||||||
|
|
||||||
return &ci, nil
|
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) {
|
func (r *clientRepo) New(tx repo.Transaction, cli client.Client) (*oidc.ClientCredentials, error) {
|
||||||
cim, err := newClientModel(cli)
|
cim, err := newClientModel(cli)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -328,3 +349,11 @@ func (r *clientRepo) SetTrustedPeers(tx repo.Transaction, clientID string, clien
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mustParseURL(s string) url.URL {
|
||||||
|
u, err := url.Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return *u
|
||||||
|
}
|
||||||
|
|
|
@ -16,7 +16,8 @@ CREATE TABLE client_identity (
|
||||||
id text NOT NULL UNIQUE,
|
id text NOT NULL UNIQUE,
|
||||||
secret blob,
|
secret blob,
|
||||||
metadata text,
|
metadata text,
|
||||||
dex_admin integer
|
dex_admin integer,
|
||||||
|
public integer
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE connector_config (
|
CREATE TABLE connector_config (
|
||||||
|
|
|
@ -48,6 +48,22 @@ func TestGetPlannedMigrations(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMigrateClientMetadata(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")
|
dsn := os.Getenv("DEX_TEST_DSN")
|
||||||
if dsn == "" {
|
if dsn == "" {
|
||||||
t.Skip("Test will not run without DEX_TEST_DSN environment variable.")
|
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 {
|
for i, tt := range tests {
|
||||||
model := &clientModel{
|
model := &oldClientModel{
|
||||||
ID: strconv.Itoa(i),
|
ID: strconv.Itoa(i),
|
||||||
Secret: []byte("verysecret"),
|
Secret: []byte("verysecret"),
|
||||||
Metadata: tt.before,
|
Metadata: tt.before,
|
||||||
|
@ -108,12 +124,12 @@ func TestMigrateClientMetadata(t *testing.T) {
|
||||||
|
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
id := strconv.Itoa(i)
|
id := strconv.Itoa(i)
|
||||||
m, err := dbMap.Get(clientModel{}, id)
|
m, err := dbMap.Get(oldClientModel{}, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("case %d: failed to get model: %v", i, err)
|
t.Errorf("case %d: failed to get model: %v", i, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
cim, ok := m.(*clientModel)
|
cim, ok := m.(*oldClientModel)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("case %d: unrecognized model type: %T", i, m)
|
t.Errorf("case %d: unrecognized model type: %T", i, m)
|
||||||
continue
|
continue
|
||||||
|
|
4
db/migrations/0013_add_public_clients.sql
Normal file
4
db/migrations/0013_add_public_clients.sql
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
-- +migrate Up
|
||||||
|
ALTER TABLE client_identity ADD COLUMN "public" boolean;
|
||||||
|
|
||||||
|
UPDATE "client_identity" SET "public" = false;
|
|
@ -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",
|
"-- +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",
|
Id: "0013_add_scopes_to_refresh_tokens.sql",
|
||||||
Up: []string{
|
Up: []string{
|
||||||
|
|
|
@ -68,7 +68,7 @@ func (fi bindataFileInfo) Sys() interface{} {
|
||||||
return nil
|
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) {
|
func dataIndexHtmlBytes() ([]byte, error) {
|
||||||
return bindataRead(
|
return bindataRead(
|
||||||
|
@ -83,7 +83,7 @@ func dataIndexHtml() (*asset, error) {
|
||||||
return nil, err
|
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}
|
a := &asset{bytes: bytes, info: info}
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<form action="/login">
|
<form action="/login">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -7,15 +8,29 @@
|
||||||
<br>
|
<br>
|
||||||
(comma-separated list of client-ids)
|
(comma-separated list of client-ids)
|
||||||
</td>
|
</td>
|
||||||
<td> <input type="text" name="cross_client"> </td>
|
<td> <input type="text" name="cross_client" > </td>
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</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>
|
</form>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ if not .OOB }}
|
||||||
<form action="/register">
|
<form action="/register">
|
||||||
<input type="submit" value="Register">
|
<input type="submit" value="Register" />
|
||||||
</form>
|
</form>
|
||||||
|
{{ end }}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"github.com/coreos/go-oidc/oauth2"
|
"github.com/coreos/go-oidc/oauth2"
|
||||||
"github.com/coreos/go-oidc/oidc"
|
"github.com/coreos/go-oidc/oidc"
|
||||||
|
|
||||||
|
"github.com/coreos/dex/client"
|
||||||
pflag "github.com/coreos/dex/pkg/flag"
|
pflag "github.com/coreos/dex/pkg/flag"
|
||||||
phttp "github.com/coreos/dex/pkg/http"
|
phttp "github.com/coreos/dex/pkg/http"
|
||||||
"github.com/coreos/dex/pkg/log"
|
"github.com/coreos/dex/pkg/log"
|
||||||
|
@ -163,15 +164,21 @@ func main() {
|
||||||
func NewClientHandler(c *oidc.Client, issuer string, cbURL url.URL) http.Handler {
|
func NewClientHandler(c *oidc.Client, issuer string, cbURL url.URL) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
oob := cbURL.String() == client.OOBRedirectURI
|
||||||
|
|
||||||
issuerURL, err := url.Parse(issuer)
|
issuerURL, err := url.Parse(issuer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Could not parse issuer url: %v", err)
|
log.Fatalf("Could not parse issuer url: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mux.HandleFunc("/", handleIndex)
|
mux.HandleFunc("/", handleIndexFunc(oob))
|
||||||
mux.HandleFunc("/login", handleLoginFunc(c))
|
mux.HandleFunc("/login", handleLoginFunc(c))
|
||||||
mux.HandleFunc("/register", handleRegisterFunc(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 := *issuerURL
|
||||||
resendURL.Path = "/resend-verify-email"
|
resendURL.Path = "/resend-verify-email"
|
||||||
|
@ -180,12 +187,16 @@ func NewClientHandler(c *oidc.Client, issuer string, cbURL url.URL) http.Handler
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleIndex(w http.ResponseWriter, r *http.Request) {
|
func handleIndexFunc(oob bool) http.HandlerFunc {
|
||||||
err := indexTemplate.Execute(w, nil)
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if err != nil {
|
err := indexTemplate.Execute(w, map[string]interface{}{
|
||||||
phttp.WriteError(w, http.StatusInternalServerError,
|
"OOB": oob,
|
||||||
fmt.Sprintf("unable to execute template: %v", err))
|
})
|
||||||
|
if err != nil {
|
||||||
|
phttp.WriteError(w, http.StatusInternalServerError,
|
||||||
|
fmt.Sprintf("unable to execute template: %v", err))
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -383,7 +383,11 @@ func TestCreateClient(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
addIDAndSecret := func(cli adminschema.Client) *adminschema.Client {
|
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"))
|
cli.Secret = base64.URLEncoding.EncodeToString([]byte("client_0"))
|
||||||
return &cli
|
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 := adminClientGood
|
||||||
adminAdminClient.IsAdmin = true
|
adminAdminClient.IsAdmin = true
|
||||||
clientGoodAdmin := clientGood
|
clientGoodAdmin := clientGood
|
||||||
|
@ -479,6 +500,27 @@ func TestCreateClient(t *testing.T) {
|
||||||
wantClient: clientGood,
|
wantClient: clientGood,
|
||||||
wantTrustedPeers: []string{"test_client_0"},
|
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 {
|
for i, tt := range tests {
|
||||||
|
@ -530,6 +572,7 @@ func TestCreateClient(t *testing.T) {
|
||||||
repoClient, err := f.cr.Get(nil, resp.Client.Id)
|
repoClient, err := f.cr.Get(nil, resp.Client.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("case %d: Unexpected error getting client: %v", i, err)
|
t.Errorf("case %d: Unexpected error getting client: %v", i, err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if diff := pretty.Compare(tt.wantClient, repoClient); diff != "" {
|
if diff := pretty.Compare(tt.wantClient, repoClient); diff != "" {
|
||||||
|
|
|
@ -303,30 +303,69 @@ func TestHTTPClientCredsToken(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cis := []client.LoadableClient{{Client: ci}}
|
|
||||||
|
|
||||||
srv, err := mockServer(cis)
|
ci2 := ci
|
||||||
if err != nil {
|
ci2.Credentials.ID = "not_a_client"
|
||||||
t.Fatalf("Unexpected error setting up server: %v", err)
|
|
||||||
|
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)
|
for i, tt := range tests {
|
||||||
if err != nil {
|
srv, err := mockServer(tt.clients)
|
||||||
t.Fatalf("Unexpected error setting up OIDC client: %v", err)
|
if err != nil {
|
||||||
}
|
t.Fatalf("case %d: Unexpected error setting up server: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
tok, err := cl.ClientCredsToken([]string{"openid"})
|
cl, err := mockClient(srv, tt.cli)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed getting client token: %v", err)
|
t.Fatalf("case %d: Unexpected error setting up OIDC client: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
claims, err := tok.Claims()
|
tok, err := cl.ClientCredsToken([]string{"openid"})
|
||||||
if err != nil {
|
if tt.wantErr {
|
||||||
t.Fatalf("Failed parsing claims from client token: %v", err)
|
if err == nil {
|
||||||
}
|
t.Errorf("case %d: want non-nil error", i)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if err := verifyUserClaims(claims, &ci, nil, srv.IssuerURL); err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to verify claims: %v", err)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 ) .,
|
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.,
|
id: string // The client ID. Ignored in client create requests.,
|
||||||
isAdmin: boolean,
|
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 ) .,
|
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: [
|
redirectURIs: [
|
||||||
string
|
string
|
||||||
],
|
],
|
||||||
|
|
|
@ -24,6 +24,7 @@ func MapSchemaClientToClient(sc Client) (client.Client, error) {
|
||||||
Metadata: oidc.ClientMetadata{
|
Metadata: oidc.ClientMetadata{
|
||||||
RedirectURIs: make([]url.URL, len(sc.RedirectURIs)),
|
RedirectURIs: make([]url.URL, len(sc.RedirectURIs)),
|
||||||
},
|
},
|
||||||
|
Public: sc.Public,
|
||||||
}
|
}
|
||||||
for i, ru := range sc.RedirectURIs {
|
for i, ru := range sc.RedirectURIs {
|
||||||
if ru == "" {
|
if ru == "" {
|
||||||
|
@ -65,6 +66,8 @@ func MapClientToSchemaClient(c client.Client) Client {
|
||||||
Id: c.Credentials.ID,
|
Id: c.Credentials.ID,
|
||||||
Secret: c.Credentials.Secret,
|
Secret: c.Credentials.Secret,
|
||||||
RedirectURIs: make([]string, len(c.Metadata.RedirectURIs)),
|
RedirectURIs: make([]string, len(c.Metadata.RedirectURIs)),
|
||||||
|
IsAdmin: c.Admin,
|
||||||
|
Public: c.Public,
|
||||||
}
|
}
|
||||||
for i, u := range c.Metadata.RedirectURIs {
|
for i, u := range c.Metadata.RedirectURIs {
|
||||||
cl.RedirectURIs[i] = u.String()
|
cl.RedirectURIs[i] = u.String()
|
||||||
|
@ -78,6 +81,5 @@ func MapClientToSchemaClient(c client.Client) Client {
|
||||||
if c.Metadata.ClientURI != nil {
|
if c.Metadata.ClientURI != nil {
|
||||||
cl.ClientURI = c.Metadata.ClientURI.String()
|
cl.ClientURI = c.Metadata.ClientURI.String()
|
||||||
}
|
}
|
||||||
cl.IsAdmin = c.Admin
|
|
||||||
return cl
|
return cl
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,23 @@ func TestMapSchemaClientToClient(t *testing.T) {
|
||||||
ClientURI: mustParseURL(t, "https://clientURI.example.com"),
|
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{
|
sc: Client{
|
||||||
Id: "123",
|
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 {
|
for i, tt := range tests {
|
||||||
|
|
|
@ -110,10 +110,11 @@ type Admin struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
// ClientName: OPTIONAL. Name of the Client to be presented to the
|
// ClientName: OPTIONAL for normal cliens. Name of the Client to be
|
||||||
// End-User. If desired, representation of this Claim in different
|
// presented to the End-User. If desired, representation of this Claim
|
||||||
// languages and scripts is represented as described in Section 2.1 (
|
// in different languages and scripts is represented as described in
|
||||||
// Metadata Languages and Scripts ) .
|
// Section 2.1 ( Metadata Languages and Scripts ). REQUIRED for public
|
||||||
|
// clients
|
||||||
ClientName string `json:"clientName,omitempty"`
|
ClientName string `json:"clientName,omitempty"`
|
||||||
|
|
||||||
// ClientURI: OPTIONAL. URL of the home page of the Client. The value of
|
// 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 ) .
|
// Section 2.1 ( Metadata Languages and Scripts ) .
|
||||||
LogoURI string `json:"logoURI,omitempty"`
|
LogoURI string `json:"logoURI,omitempty"`
|
||||||
|
|
||||||
// RedirectURIs: REQUIRED. Array of Redirection URI values used by the
|
// Public: OPTIONAL. Determines if the client is public. Public clients
|
||||||
// Client. One of these registered Redirection URI values MUST exactly
|
// have certain restrictions: They cannot use their credentials to
|
||||||
// match the redirect_uri parameter value used in each Authorization
|
// obtain a client JWT. Their redirects URLs cannot be specified: they
|
||||||
// Request, with the matching performed as described in Section 6.2.1 of
|
// are always http://localhost:$PORT or urn:ietf:wg:oauth:2.0:oob
|
||||||
// [RFC3986] ( Berners-Lee, T., Fielding, R., and L. Masinter,
|
Public bool `json:"public,omitempty"`
|
||||||
// “Uniform Resource Identifier (URI): Generic Syntax,” January
|
|
||||||
// 2005. ) (Simple String Comparison).
|
// 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"`
|
RedirectURIs []string `json:"redirectURIs,omitempty"`
|
||||||
|
|
||||||
// Secret: The client secret. Ignored in client create requests.
|
// Secret: The client secret. Ignored in client create requests.
|
||||||
|
|
|
@ -72,11 +72,11 @@ const DiscoveryJSON = `{
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"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": {
|
"clientName": {
|
||||||
"type": "string",
|
"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": {
|
"logoURI": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -92,6 +92,10 @@ const DiscoveryJSON = `{
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"description": "Array of ClientIDs of clients that are allowed to mint ID tokens for the client being created."
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -65,11 +65,11 @@
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"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": {
|
"clientName": {
|
||||||
"type": "string",
|
"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": {
|
"logoURI": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -85,6 +85,10 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"description": "Array of ClientIDs of clients that are allowed to mint ID tokens for the client being created."
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -299,35 +299,23 @@ func getTemplates(issuerName, issuerLogoURL string,
|
||||||
}
|
}
|
||||||
|
|
||||||
func setTemplates(srv *Server, tpls *template.Template) error {
|
func setTemplates(srv *Server, tpls *template.Template) error {
|
||||||
ltpl, err := findTemplate(LoginPageTemplateName, tpls)
|
for _, t := range []struct {
|
||||||
if err != nil {
|
templateName string
|
||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@ var (
|
||||||
httpPathAcceptInvitation = "/accept-invitation"
|
httpPathAcceptInvitation = "/accept-invitation"
|
||||||
httpPathDebugVars = "/debug/vars"
|
httpPathDebugVars = "/debug/vars"
|
||||||
httpPathClientRegistration = "/registration"
|
httpPathClientRegistration = "/registration"
|
||||||
|
httpPathOOB = "/oob"
|
||||||
|
|
||||||
cookieLastSeen = "LastSeen"
|
cookieLastSeen = "LastSeen"
|
||||||
cookieShowEmailVerifiedMessage = "ShowEmailVerifiedMessage"
|
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.
|
// Render error message if client id is invalid.
|
||||||
clientID := q.Get("client_id")
|
clientID := q.Get("client_id")
|
||||||
cm, err := srv.ClientMetadata(clientID)
|
_, err := srv.Client(clientID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed fetching client %q from repo: %v", clientID, err)
|
log.Errorf("Failed fetching client %q from repo: %v", clientID, err)
|
||||||
td.Error = true
|
td.Error = true
|
||||||
|
@ -196,7 +197,7 @@ func renderLoginPage(w http.ResponseWriter, r *http.Request, srv OIDCServer, idp
|
||||||
execTemplate(w, tpl, td)
|
execTemplate(w, tpl, td)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cm == nil {
|
if err == client.ErrorNotFound {
|
||||||
td.Error = true
|
td.Error = true
|
||||||
td.Message = "Authentication Error"
|
td.Message = "Authentication Error"
|
||||||
td.Detail = "Invalid client ID"
|
td.Detail = "Invalid client ID"
|
||||||
|
@ -299,25 +300,19 @@ func handleAuthFunc(srv OIDCServer, idpcs []connector.Connector, tpl *template.T
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cm, err := srv.ClientMetadata(acr.ClientID)
|
cli, err := srv.Client(acr.ClientID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed fetching client %q from repo: %v", acr.ClientID, err)
|
log.Errorf("Failed fetching client %q from repo: %v", acr.ClientID, err)
|
||||||
writeAuthError(w, oauth2.NewError(oauth2.ErrorServerError), acr.State)
|
writeAuthError(w, oauth2.NewError(oauth2.ErrorServerError), acr.State)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cm == nil {
|
if err == client.ErrorNotFound {
|
||||||
log.Errorf("Client %q not found", acr.ClientID)
|
log.Errorf("Client %q not found", acr.ClientID)
|
||||||
writeAuthError(w, oauth2.NewError(oauth2.ErrorInvalidRequest), acr.State)
|
writeAuthError(w, oauth2.NewError(oauth2.ErrorInvalidRequest), acr.State)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cm.RedirectURIs) == 0 {
|
redirectURL, err := cli.ValidRedirectURL(acr.RedirectURL)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
switch err {
|
||||||
case (client.ErrorCantChooseRedirectURL):
|
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 {
|
func makeHealthHandler(checks []health.Checkable) http.Handler {
|
||||||
return health.Checker{
|
return health.Checker{
|
||||||
Checks: checks,
|
Checks: checks,
|
||||||
|
|
|
@ -105,6 +105,30 @@ func TestHandleAuthFuncResponsesSingleRedirectURL(t *testing.T) {
|
||||||
wantLocation: "http://fake.example.com",
|
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
|
// provided redirect_uri does not match client
|
||||||
{
|
{
|
||||||
query: url.Values{
|
query: url.Values{
|
||||||
|
@ -173,6 +197,17 @@ func TestHandleAuthFuncResponsesSingleRedirectURL(t *testing.T) {
|
||||||
},
|
},
|
||||||
wantCode: http.StatusBadRequest,
|
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 {
|
for i, tt := range tests {
|
||||||
|
|
|
@ -38,12 +38,12 @@ const (
|
||||||
VerifyEmailTemplateName = "verify-email.html"
|
VerifyEmailTemplateName = "verify-email.html"
|
||||||
SendResetPasswordEmailTemplateName = "send-reset-password.html"
|
SendResetPasswordEmailTemplateName = "send-reset-password.html"
|
||||||
ResetPasswordTemplateName = "reset-password.html"
|
ResetPasswordTemplateName = "reset-password.html"
|
||||||
|
OOBTemplateName = "oob-template.html"
|
||||||
APIVersion = "v1"
|
APIVersion = "v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OIDCServer interface {
|
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)
|
NewSession(connectorID, clientID, clientState string, redirectURL url.URL, nonce string, register bool, scope []string) (string, error)
|
||||||
Login(oidc.Identity, string) (string, error)
|
Login(oidc.Identity, string) (string, error)
|
||||||
|
|
||||||
|
@ -72,6 +72,7 @@ type Server struct {
|
||||||
VerifyEmailTemplate *template.Template
|
VerifyEmailTemplate *template.Template
|
||||||
SendResetPasswordEmailTemplate *template.Template
|
SendResetPasswordEmailTemplate *template.Template
|
||||||
ResetPasswordTemplate *template.Template
|
ResetPasswordTemplate *template.Template
|
||||||
|
OOBTemplate *template.Template
|
||||||
|
|
||||||
HealthChecks []health.Checkable
|
HealthChecks []health.Checkable
|
||||||
Connectors []connector.Connector
|
Connectors []connector.Connector
|
||||||
|
@ -214,6 +215,7 @@ func (s *Server) HTTPHandler() http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc(httpPathDiscovery, handleDiscoveryFunc(s.ProviderConfig()))
|
mux.HandleFunc(httpPathDiscovery, handleDiscoveryFunc(s.ProviderConfig()))
|
||||||
mux.HandleFunc(httpPathAuth, handleAuthFunc(s, s.Connectors, s.LoginTemplate, s.EnableRegistration))
|
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(httpPathToken, handleTokenFunc(s))
|
||||||
mux.HandleFunc(httpPathKeys, handleKeysFunc(s.KeyManager, clock))
|
mux.HandleFunc(httpPathKeys, handleKeysFunc(s.KeyManager, clock))
|
||||||
mux.Handle(httpPathHealth, makeHealthHandler(checks))
|
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) {
|
func (s *Server) Client(clientID string) (client.Client, error) {
|
||||||
return s.ClientManager.Metadata(clientID)
|
return s.ClientManager.Get(clientID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) NewSession(ipdcID, clientID, clientState string, redirectURL url.URL, nonce string, register bool, scope []string) (string, error) {
|
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
|
ru := ses.RedirectURL
|
||||||
|
if ru.String() == client.OOBRedirectURI {
|
||||||
|
ru = s.absURL(httpPathOOB)
|
||||||
|
}
|
||||||
q := ru.Query()
|
q := ru.Query()
|
||||||
q.Set("code", code)
|
q.Set("code", code)
|
||||||
q.Set("state", ses.ClientState)
|
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) {
|
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)
|
ok, err := s.ClientManager.Authenticate(creds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed fetching client %s from manager: %v", creds.ID, err)
|
log.Errorf("Failed fetching client %s from manager: %v", creds.ID, err)
|
||||||
|
|
|
@ -39,6 +39,13 @@ var (
|
||||||
ID: testClientID,
|
ID: testClientID,
|
||||||
Secret: clientTestSecret,
|
Secret: clientTestSecret,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testPublicClientID = "publicclient.example.com"
|
||||||
|
publicClientTestSecret = base64.URLEncoding.EncodeToString([]byte("secret"))
|
||||||
|
testPublicClientCredentials = oidc.ClientCredentials{
|
||||||
|
ID: testPublicClientID,
|
||||||
|
Secret: publicClientTestSecret,
|
||||||
|
}
|
||||||
testClients = []client.LoadableClient{
|
testClients = []client.LoadableClient{
|
||||||
{
|
{
|
||||||
Client: client.Client{
|
Client: client.Client{
|
||||||
|
@ -50,6 +57,12 @@ var (
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Client: client.Client{
|
||||||
|
Credentials: testPublicClientCredentials,
|
||||||
|
Public: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
testConnectorID1 = "IDPC-1"
|
testConnectorID1 = "IDPC-1"
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"id": "XXX",
|
"id": "XXX",
|
||||||
"secret": "c2VjcmV0ZQ==",
|
"secret": "c2VjcmV0ZQ==",
|
||||||
"redirectURLs": ["http://127.0.0.1:5555/callback"],
|
"redirectURLs": ["http://127.0.0.1:5555/callback"],
|
||||||
"trustedPeers": ["example-app"]
|
"trustedPeers": ["example-app", "public"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "example-app",
|
"id": "example-app",
|
||||||
|
@ -15,6 +15,11 @@
|
||||||
"secret": "ZXhhbXBsZS1jbGktc2VjcmV0",
|
"secret": "ZXhhbXBsZS1jbGktc2VjcmV0",
|
||||||
"redirectURLs": ["http://127.0.0.1:8000/admin/v1/oauth/login"]
|
"redirectURLs": ["http://127.0.0.1:8000/admin/v1/oauth/login"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "public",
|
||||||
|
"secret": "ZXhhbXBsZS1hcHAtc2VjcmV0",
|
||||||
|
"public": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "oauth2_proxy",
|
"id": "oauth2_proxy",
|
||||||
"secret": "cHJveHk=",
|
"secret": "cHJveHk=",
|
||||||
|
|
11
static/html/oob-template.html
Normal file
11
static/html/oob-template.html
Normal 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" }}
|
Loading…
Reference in a new issue