server: add dynamic client registration

This commit is contained in:
Eric Chiang 2016-01-21 16:07:41 -08:00
parent 9796a1e648
commit 04cd1851aa
6 changed files with 271 additions and 42 deletions

View file

@ -45,6 +45,7 @@ func main() {
emailConfig := fs.String("email-cfg", "./static/fixtures/emailer.json", "configures emailer.") emailConfig := fs.String("email-cfg", "./static/fixtures/emailer.json", "configures emailer.")
enableRegistration := fs.Bool("enable-registration", false, "Allows users to self-register") enableRegistration := fs.Bool("enable-registration", false, "Allows users to self-register")
enableClientRegistration := fs.Bool("enable-client-registration", false, "Allow dynamic registration of clients")
noDB := fs.Bool("no-db", false, "manage entities in-process w/o any encryption, used only for single-node testing") noDB := fs.Bool("no-db", false, "manage entities in-process w/o any encryption, used only for single-node testing")
@ -125,6 +126,7 @@ func main() {
IssuerName: *issuerName, IssuerName: *issuerName,
IssuerLogoURL: *issuerLogoURL, IssuerLogoURL: *issuerLogoURL,
EnableRegistration: *enableRegistration, EnableRegistration: *enableRegistration,
EnableClientRegistration: *enableClientRegistration,
} }
if *noDB { if *noDB {

View file

@ -0,0 +1,57 @@
package server
import (
"encoding/json"
"net/http"
"github.com/coreos/dex/pkg/log"
"github.com/coreos/go-oidc/oauth2"
"github.com/coreos/go-oidc/oidc"
)
const (
invalidRedirectURI = "invalid_redirect_uri"
invalidClientMetadata = "invalid_client_metadata"
)
func (s *Server) handleClientRegistration(w http.ResponseWriter, r *http.Request) {
resp, err := s.handleClientRegistrationRequest(r)
if err != nil {
code := http.StatusBadRequest
if err.Type == oauth2.ErrorServerError {
code = http.StatusInternalServerError
}
writeResponseWithBody(w, code, err)
} else {
writeResponseWithBody(w, http.StatusCreated, resp)
}
}
func (s *Server) handleClientRegistrationRequest(r *http.Request) (*oidc.ClientRegistrationResponse, *apiError) {
var clientMetadata oidc.ClientMetadata
if err := json.NewDecoder(r.Body).Decode(&clientMetadata); err != nil {
return nil, newAPIError(oauth2.ErrorInvalidRequest, err.Error())
}
if err := s.ProviderConfig().Supports(clientMetadata); err != nil {
return nil, newAPIError(invalidClientMetadata, err.Error())
}
// metadata is guarenteed to have at least one redirect_uri by earlier validation.
id, err := oidc.GenClientID(clientMetadata.RedirectURIs[0].Host)
if err != nil {
log.Errorf("Faild to create client ID: %v", err)
return nil, newAPIError(oauth2.ErrorServerError, "unable to save client metadata")
}
creds, err := s.ClientIdentityRepo.New(id, clientMetadata)
if err != nil {
log.Errorf("Failed to create new client identity: %v", err)
return nil, newAPIError(oauth2.ErrorServerError, "unable to save client metadata")
}
return &oidc.ClientRegistrationResponse{
ClientID: creds.ID,
ClientSecret: creds.Secret,
ClientMetadata: clientMetadata,
}, nil
}

View file

@ -0,0 +1,161 @@
package server
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/coreos/go-oidc/oauth2"
"github.com/coreos/go-oidc/oidc"
"github.com/kylelemons/godebug/pretty"
)
func TestClientRegistration(t *testing.T) {
tests := []struct {
body string
code int
}{
{"", http.StatusBadRequest},
{
`{
"redirect_uris": [
"https://client.example.org/callback",
"https://client.example.org/callback2"
]
}`,
http.StatusCreated,
},
{
// Requesting unsupported client metadata fields (user_info_encrypted).
`{
"application_type": "web",
"redirect_uris":[
"https://client.example.org/callback",
"https://client.example.org/callback2"
],
"client_name": "My Example",
"logo_uri": "https://client.example.org/logo.png",
"subject_type": "pairwise",
"sector_identifier_uri": "https://other.example.net/file_of_redirect_uris.json",
"token_endpoint_auth_method": "client_secret_basic",
"jwks_uri": "https://client.example.org/my_public_keys.jwks",
"userinfo_encrypted_response_alg": "RSA1_5",
"userinfo_encrypted_response_enc": "A128CBC-HS256",
"contacts": ["ve7jtb@example.org", "mary@example.org"],
"request_uris": [
"https://client.example.org/rf.txt#qpXaRLh_n93TTR9F252ValdatUQvQiJi5BDub2BeznA" ]
}`,
http.StatusBadRequest,
},
{
`{
"application_type": "web",
"redirect_uris":[
"https://client.example.org/callback",
"https://client.example.org/callback2"
],
"client_name": "My Example",
"logo_uri": "https://client.example.org/logo.png",
"subject_type": "pairwise",
"sector_identifier_uri": "https://other.example.net/file_of_redirect_uris.json",
"token_endpoint_auth_method": "client_secret_basic",
"jwks_uri": "https://client.example.org/my_public_keys.jwks",
"contacts": ["ve7jtb@example.org", "mary@example.org"],
"request_uris": [
"https://client.example.org/rf.txt#qpXaRLh_n93TTR9F252ValdatUQvQiJi5BDub2BeznA" ]
}`,
http.StatusCreated,
},
}
var handler http.Handler
f := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handler.ServeHTTP(w, r)
})
testServer := httptest.NewServer(f)
issuerURL, err := url.Parse(testServer.URL)
if err != nil {
t.Fatal(err)
}
defer testServer.Close()
for i, tt := range tests {
fixtures, err := makeTestFixtures()
if err != nil {
t.Fatal(err)
}
fixtures.srv.IssuerURL = *issuerURL
fixtures.srv.EnableClientRegistration = true
handler = fixtures.srv.HTTPHandler()
err = func() error {
// GET provider config through discovery URL.
resp, err := http.Get(testServer.URL + "/.well-known/openid-configuration")
if err != nil {
return fmt.Errorf("GET config: %v", err)
}
var cfg oidc.ProviderConfig
err = json.NewDecoder(resp.Body).Decode(&cfg)
resp.Body.Close()
if err != nil {
return fmt.Errorf("decode resp: %v", err)
}
if cfg.RegistrationEndpoint == nil {
return errors.New("registration endpoint not available")
}
// POST registration request to registration endpoint.
body := strings.NewReader(tt.body)
resp, err = http.Post(cfg.RegistrationEndpoint.String(), "application/json", body)
if err != nil {
return fmt.Errorf("POSTing client metadata: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != tt.code {
return fmt.Errorf("expected status code=%d, got=%d", tt.code, resp.StatusCode)
}
if resp.StatusCode != http.StatusCreated {
var oauthErr oauth2.Error
if err := json.NewDecoder(resp.Body).Decode(&oauthErr); err != nil {
return fmt.Errorf("failed to decode oauth2 error: %v", err)
}
if oauthErr.Type == "" {
return fmt.Errorf("got oauth2 error with no 'error' field")
}
return nil
}
// Read registration response.
var r oidc.ClientRegistrationResponse
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
return fmt.Errorf("decode response: %v", err)
}
if r.ClientID == "" {
return fmt.Errorf("no client id in registration response")
}
metadata, err := fixtures.clientIdentityRepo.Metadata(r.ClientID)
if err != nil {
return fmt.Errorf("failed to lookup client id after creation")
}
if diff := pretty.Compare(&metadata, &r.ClientMetadata); diff != "" {
return fmt.Errorf("metadata in response did not match metadata in db: %s", diff)
}
return nil
}()
if err != nil {
t.Errorf("case %d: %v", i, err)
}
}
}

View file

@ -35,6 +35,7 @@ type ServerConfig struct {
EmailerConfigFile string EmailerConfigFile string
StateConfig StateConfigurer StateConfig StateConfigurer
EnableRegistration bool EnableRegistration bool
EnableClientRegistration bool
} }
type StateConfigurer interface { type StateConfigurer interface {
@ -74,6 +75,7 @@ func (cfg *ServerConfig) Server() (*Server, error) {
Connectors: []connector.Connector{}, Connectors: []connector.Connector{},
EnableRegistration: cfg.EnableRegistration, EnableRegistration: cfg.EnableRegistration,
EnableClientRegistration: cfg.EnableClientRegistration,
} }
err = cfg.StateConfig.Configure(&srv) err = cfg.StateConfig.Configure(&srv)

View file

@ -42,6 +42,7 @@ var (
httpPathResetPassword = "/reset-password" httpPathResetPassword = "/reset-password"
httpPathAcceptInvitation = "/accept-invitation" httpPathAcceptInvitation = "/accept-invitation"
httpPathDebugVars = "/debug/vars" httpPathDebugVars = "/debug/vars"
httpPathClientRegistration = "/registration"
cookieLastSeen = "LastSeen" cookieLastSeen = "LastSeen"
cookieShowEmailVerifiedMessage = "ShowEmailVerifiedMessage" cookieShowEmailVerifiedMessage = "ShowEmailVerifiedMessage"

View file

@ -74,6 +74,7 @@ type Server struct {
RefreshTokenRepo refresh.RefreshTokenRepo RefreshTokenRepo refresh.RefreshTokenRepo
UserEmailer *useremail.UserEmailer UserEmailer *useremail.UserEmailer
EnableRegistration bool EnableRegistration bool
EnableClientRegistration bool
localConnectorID string localConnectorID string
} }
@ -110,19 +111,15 @@ func (s *Server) KillSession(sessionKey string) error {
return err return err
} }
func (s *Server) pathURL(path string) *url.URL {
u := s.IssuerURL
u.Path = path
return &u
}
func (s *Server) ProviderConfig() oidc.ProviderConfig { func (s *Server) ProviderConfig() oidc.ProviderConfig {
authEndpoint := s.absURL(httpPathAuth)
tokenEndpoint := s.absURL(httpPathToken)
keysEndpoint := s.absURL(httpPathKeys)
cfg := oidc.ProviderConfig{ cfg := oidc.ProviderConfig{
Issuer: &s.IssuerURL, Issuer: &s.IssuerURL,
AuthEndpoint: &authEndpoint,
AuthEndpoint: s.pathURL(httpPathAuth), TokenEndpoint: &tokenEndpoint,
TokenEndpoint: s.pathURL(httpPathToken), KeysEndpoint: &keysEndpoint,
KeysEndpoint: s.pathURL(httpPathKeys),
GrantTypesSupported: []string{oauth2.GrantTypeAuthCode, oauth2.GrantTypeClientCreds}, GrantTypesSupported: []string{oauth2.GrantTypeAuthCode, oauth2.GrantTypeClientCreds},
ResponseTypesSupported: []string{"code"}, ResponseTypesSupported: []string{"code"},
@ -131,6 +128,11 @@ func (s *Server) ProviderConfig() oidc.ProviderConfig {
TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"}, TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"},
} }
if s.EnableClientRegistration {
regEndpoint := s.absURL(httpPathClientRegistration)
cfg.RegistrationEndpoint = &regEndpoint
}
return cfg return cfg
} }
@ -246,6 +248,10 @@ func (s *Server) HTTPHandler() http.Handler {
redirectValidityWindow: s.SessionManager.ValidityWindow, redirectValidityWindow: s.SessionManager.ValidityWindow,
}) })
if s.EnableClientRegistration {
mux.HandleFunc(httpPathClientRegistration, s.handleClientRegistration)
}
mux.HandleFunc(httpPathDebugVars, health.ExpvarHandler) mux.HandleFunc(httpPathDebugVars, health.ExpvarHandler)
pcfg := s.ProviderConfig() pcfg := s.ProviderConfig()