Merge pull request #479 from bobbyrullo/specify_yer_client_creds

Allow specification of client_{id, secret} in admin api
This commit is contained in:
bobbyrullo 2016-06-21 13:25:25 -07:00 committed by GitHub
commit a7b860b9c2
8 changed files with 108 additions and 45 deletions

View file

@ -78,6 +78,8 @@ var (
client.ErrorPublicClientMissingName: errorMaker("bad_request", "Public clients require a ClientName", http.StatusBadRequest), client.ErrorPublicClientMissingName: errorMaker("bad_request", "Public clients require a ClientName", http.StatusBadRequest),
client.ErrorInvalidClientSecret: errorMaker("bad_request", "Secret must be a base64 encoded string", http.StatusBadRequest),
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),

View file

@ -16,7 +16,10 @@ import (
) )
var ( var (
ErrorInvalidClientID = errors.New("not a valid client ID") ErrorInvalidClientID = errors.New("not a valid client ID")
ErrorInvalidClientSecret = errors.New("not a valid client Secret")
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.")
@ -46,7 +49,7 @@ const (
func HashSecret(creds oidc.ClientCredentials) ([]byte, error) { func HashSecret(creds oidc.ClientCredentials) ([]byte, error) {
secretBytes, err := base64.URLEncoding.DecodeString(creds.Secret) secretBytes, err := base64.URLEncoding.DecodeString(creds.Secret)
if err != nil { if err != nil {
return nil, err return nil, ErrorInvalidClientSecret
} }
hashed, err := bcrypt.GenerateFromPassword([]byte( hashed, err := bcrypt.GenerateFromPassword([]byte(
secretBytes), secretBytes),

View file

@ -196,18 +196,30 @@ func (m *ClientManager) addClientCredentials(cli *client.Client) error {
seed = cli.Metadata.RedirectURIs[0].Host seed = cli.Metadata.RedirectURIs[0].Host
} }
// Generate Client ID var err error
clientID, err := m.clientIDGenerator(seed) var clientID string
if err != nil { if cli.Credentials.ID != "" {
return err clientID = cli.Credentials.ID
} else {
// Generate Client ID
clientID, err = m.clientIDGenerator(seed)
if err != nil {
return err
}
} }
// Generate Secret var clientSecret string
secret, err := m.secretGenerator() if cli.Credentials.Secret != "" {
if err != nil { clientSecret = cli.Credentials.Secret
return err } else {
// Generate Secret
secret, err := m.secretGenerator()
if err != nil {
return err
}
clientSecret = base64.URLEncoding.EncodeToString(secret)
} }
clientSecret := base64.URLEncoding.EncodeToString(secret)
cli.Credentials = oidc.ClientCredentials{ cli.Credentials = oidc.ClientCredentials{
ID: clientID, ID: clientID,
Secret: clientSecret, Secret: clientSecret,

View file

@ -383,12 +383,17 @@ func TestCreateClient(t *testing.T) {
} }
addIDAndSecret := func(cli adminschema.Client) *adminschema.Client { addIDAndSecret := func(cli adminschema.Client) *adminschema.Client {
if cli.Public { if cli.Id == "" {
cli.Id = "client_" + cli.ClientName if cli.Public {
} else { cli.Id = "client_" + cli.ClientName
cli.Id = "client_auth.example.com" } else {
cli.Id = "client_auth.example.com"
}
}
if cli.Secret == "" {
cli.Secret = base64.URLEncoding.EncodeToString([]byte("client_0"))
} }
cli.Secret = base64.URLEncoding.EncodeToString([]byte("client_0"))
return &cli return &cli
} }
@ -436,6 +441,24 @@ func TestCreateClient(t *testing.T) {
adminClientWithPeers := adminClientGood adminClientWithPeers := adminClientGood
adminClientWithPeers.TrustedPeers = []string{"test_client_0"} adminClientWithPeers.TrustedPeers = []string{"test_client_0"}
adminClientOwnID := adminClientGood
adminClientOwnID.Id = "my_own_id"
clientGoodOwnID := clientGood
clientGoodOwnID.Credentials.ID = "my_own_id"
adminClientOwnSecret := adminClientGood
adminClientOwnSecret.Secret = base64.URLEncoding.EncodeToString([]byte("my_own_secret"))
clientGoodOwnSecret := clientGood
adminClientOwnIDAndSecret := adminClientGood
adminClientOwnIDAndSecret.Id = "my_own_id"
adminClientOwnIDAndSecret.Secret = base64.URLEncoding.EncodeToString([]byte("my_own_secret"))
clientGoodOwnIDAndSecret := clientGoodOwnID
adminClientBadSecret := adminClientGood
adminClientBadSecret.Secret = "not_base64_encoded"
tests := []struct { tests := []struct {
req adminschema.ClientCreateRequest req adminschema.ClientCreateRequest
want adminschema.ClientCreateResponse want adminschema.ClientCreateResponse
@ -446,24 +469,21 @@ func TestCreateClient(t *testing.T) {
{ {
req: adminschema.ClientCreateRequest{}, req: adminschema.ClientCreateRequest{},
wantError: http.StatusBadRequest, wantError: http.StatusBadRequest,
}, }, {
{
req: adminschema.ClientCreateRequest{ req: adminschema.ClientCreateRequest{
Client: &adminschema.Client{ Client: &adminschema.Client{
IsAdmin: true, IsAdmin: true,
}, },
}, },
wantError: http.StatusBadRequest, wantError: http.StatusBadRequest,
}, }, {
{
req: adminschema.ClientCreateRequest{ req: adminschema.ClientCreateRequest{
Client: &adminschema.Client{ Client: &adminschema.Client{
RedirectURIs: []string{"909090"}, RedirectURIs: []string{"909090"},
}, },
}, },
wantError: http.StatusBadRequest, wantError: http.StatusBadRequest,
}, }, {
{
req: adminschema.ClientCreateRequest{ req: adminschema.ClientCreateRequest{
Client: &adminClientGood, Client: &adminClientGood,
}, },
@ -471,8 +491,7 @@ func TestCreateClient(t *testing.T) {
Client: addIDAndSecret(adminClientGood), Client: addIDAndSecret(adminClientGood),
}, },
wantClient: clientGood, wantClient: clientGood,
}, }, {
{
req: adminschema.ClientCreateRequest{ req: adminschema.ClientCreateRequest{
Client: &adminAdminClient, Client: &adminAdminClient,
}, },
@ -480,8 +499,7 @@ func TestCreateClient(t *testing.T) {
Client: addIDAndSecret(adminAdminClient), Client: addIDAndSecret(adminAdminClient),
}, },
wantClient: clientGoodAdmin, wantClient: clientGoodAdmin,
}, }, {
{
req: adminschema.ClientCreateRequest{ req: adminschema.ClientCreateRequest{
Client: &adminMultiRedirect, Client: &adminMultiRedirect,
}, },
@ -489,8 +507,7 @@ func TestCreateClient(t *testing.T) {
Client: addIDAndSecret(adminMultiRedirect), Client: addIDAndSecret(adminMultiRedirect),
}, },
wantClient: clientMultiRedirect, wantClient: clientMultiRedirect,
}, }, {
{
req: adminschema.ClientCreateRequest{ req: adminschema.ClientCreateRequest{
Client: &adminClientWithPeers, Client: &adminClientWithPeers,
}, },
@ -499,8 +516,7 @@ func TestCreateClient(t *testing.T) {
}, },
wantClient: clientGood, wantClient: clientGood,
wantTrustedPeers: []string{"test_client_0"}, wantTrustedPeers: []string{"test_client_0"},
}, }, {
{
req: adminschema.ClientCreateRequest{ req: adminschema.ClientCreateRequest{
Client: &adminPublicClientGood, Client: &adminPublicClientGood,
}, },
@ -508,18 +524,45 @@ func TestCreateClient(t *testing.T) {
Client: addIDAndSecret(adminPublicClientGood), Client: addIDAndSecret(adminPublicClientGood),
}, },
wantClient: clientPublicGood, wantClient: clientPublicGood,
}, }, {
{
req: adminschema.ClientCreateRequest{ req: adminschema.ClientCreateRequest{
Client: &adminPublicClientMissingName, Client: &adminPublicClientMissingName,
}, },
wantError: http.StatusBadRequest, wantError: http.StatusBadRequest,
}, }, {
{
req: adminschema.ClientCreateRequest{ req: adminschema.ClientCreateRequest{
Client: &adminPublicClientHasARedirect, Client: &adminPublicClientHasARedirect,
}, },
wantError: http.StatusBadRequest, wantError: http.StatusBadRequest,
}, {
req: adminschema.ClientCreateRequest{
Client: &adminClientOwnID,
},
want: adminschema.ClientCreateResponse{
Client: addIDAndSecret(adminClientOwnID),
},
wantClient: clientGoodOwnID,
}, {
req: adminschema.ClientCreateRequest{
Client: &adminClientOwnSecret,
},
want: adminschema.ClientCreateResponse{
Client: addIDAndSecret(adminClientOwnSecret),
},
wantClient: clientGoodOwnSecret,
}, {
req: adminschema.ClientCreateRequest{
Client: &adminClientOwnIDAndSecret,
},
want: adminschema.ClientCreateResponse{
Client: addIDAndSecret(adminClientOwnIDAndSecret),
},
wantClient: clientGoodOwnIDAndSecret,
}, {
req: adminschema.ClientCreateRequest{
Client: &adminClientBadSecret,
},
wantError: http.StatusBadRequest,
}, },
} }

View file

@ -28,14 +28,14 @@ __Version:__ v1
{ {
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, 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. If specified in a client create request, it will be used as the ID. Otherwise, the server will choose the ID.,
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, 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
], ],
secret: string // The client secret. Ignored in client create requests., secret: string // The client secret. If specified in a client create request, it will be used as the secret. Otherwise, the server will choose the secret. Must be a base64 URLEncoded string.,
trustedPeers: [ trustedPeers: [
string string
] ]

View file

@ -125,7 +125,8 @@ type Client struct {
// Languages and Scripts ) . // Languages and Scripts ) .
ClientURI string `json:"clientURI,omitempty"` ClientURI string `json:"clientURI,omitempty"`
// Id: The client ID. Ignored in client create requests. // Id: The client ID. If specified in a client create request, it will
// be used as the ID. Otherwise, the server will choose the ID.
Id string `json:"id,omitempty"` Id string `json:"id,omitempty"`
IsAdmin bool `json:"isAdmin,omitempty"` IsAdmin bool `json:"isAdmin,omitempty"`
@ -141,7 +142,7 @@ type Client struct {
// Public: OPTIONAL. Determines if the client is public. Public clients // Public: OPTIONAL. Determines if the client is public. Public clients
// have certain restrictions: They cannot use their credentials to // have certain restrictions: They cannot use their credentials to
// obtain a client JWT. Their redirects URLs cannot be specified: they // obtain a client JWT. Their redirects URLs cannot be specified: they
// are always http://localhost:$PORT or urn:ietf:wg:oauth:2.0:oob // are always http://localhost:$PORT or urn:ietf:wg:oauth:2.0:oob.
Public bool `json:"public,omitempty"` Public bool `json:"public,omitempty"`
// RedirectURIs: REQUIRED for normal clients. Array of Redirection URI // RedirectURIs: REQUIRED for normal clients. Array of Redirection URI
@ -154,7 +155,9 @@ type Client struct {
// clients. // clients.
RedirectURIs []string `json:"redirectURIs,omitempty"` RedirectURIs []string `json:"redirectURIs,omitempty"`
// Secret: The client secret. Ignored in client create requests. // Secret: The client secret. If specified in a client create request,
// it will be used as the secret. Otherwise, the server will choose the
// secret. Must be a base64 URLEncoded string.
Secret string `json:"secret,omitempty"` Secret string `json:"secret,omitempty"`
// TrustedPeers: Array of ClientIDs of clients that are allowed to mint // TrustedPeers: Array of ClientIDs of clients that are allowed to mint

View file

@ -58,11 +58,11 @@ const DiscoveryJSON = `{
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
"description": "The client ID. Ignored in client create requests." "description": "The client ID. If specified in a client create request, it will be used as the ID. Otherwise, the server will choose the ID."
}, },
"secret": { "secret": {
"type": "string", "type": "string",
"description": "The client secret. Ignored in client create requests." "description": "The client secret. If specified in a client create request, it will be used as the secret. Otherwise, the server will choose the secret. Must be a base64 URLEncoded string."
}, },
"isAdmin": { "isAdmin": {
"type": "boolean" "type": "boolean"
@ -95,7 +95,7 @@ const DiscoveryJSON = `{
}, },
"public": { "public": {
"type": "boolean", "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" "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

@ -51,11 +51,11 @@
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
"description": "The client ID. Ignored in client create requests." "description": "The client ID. If specified in a client create request, it will be used as the ID. Otherwise, the server will choose the ID."
}, },
"secret": { "secret": {
"type": "string", "type": "string",
"description": "The client secret. Ignored in client create requests." "description": "The client secret. If specified in a client create request, it will be used as the secret. Otherwise, the server will choose the secret. Must be a base64 URLEncoded string."
}, },
"isAdmin": { "isAdmin": {
"type": "boolean" "type": "boolean"