forked from mystiq/dex
server: add dynamic client registration
This commit is contained in:
parent
9796a1e648
commit
04cd1851aa
6 changed files with 271 additions and 42 deletions
|
@ -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")
|
||||||
|
|
||||||
|
@ -117,14 +118,15 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
scfg := server.ServerConfig{
|
scfg := server.ServerConfig{
|
||||||
IssuerURL: *issuer,
|
IssuerURL: *issuer,
|
||||||
TemplateDir: *templates,
|
TemplateDir: *templates,
|
||||||
EmailTemplateDirs: emailTemplateDirs,
|
EmailTemplateDirs: emailTemplateDirs,
|
||||||
EmailFromAddress: *emailFrom,
|
EmailFromAddress: *emailFrom,
|
||||||
EmailerConfigFile: *emailConfig,
|
EmailerConfigFile: *emailConfig,
|
||||||
IssuerName: *issuerName,
|
IssuerName: *issuerName,
|
||||||
IssuerLogoURL: *issuerLogoURL,
|
IssuerLogoURL: *issuerLogoURL,
|
||||||
EnableRegistration: *enableRegistration,
|
EnableRegistration: *enableRegistration,
|
||||||
|
EnableClientRegistration: *enableClientRegistration,
|
||||||
}
|
}
|
||||||
|
|
||||||
if *noDB {
|
if *noDB {
|
||||||
|
|
57
server/client_registration.go
Normal file
57
server/client_registration.go
Normal 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
|
||||||
|
}
|
161
server/client_registration_test.go
Normal file
161
server/client_registration_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,15 +26,16 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
IssuerURL string
|
IssuerURL string
|
||||||
IssuerName string
|
IssuerName string
|
||||||
IssuerLogoURL string
|
IssuerLogoURL string
|
||||||
TemplateDir string
|
TemplateDir string
|
||||||
EmailTemplateDirs []string
|
EmailTemplateDirs []string
|
||||||
EmailFromAddress string
|
EmailFromAddress string
|
||||||
EmailerConfigFile string
|
EmailerConfigFile string
|
||||||
StateConfig StateConfigurer
|
StateConfig StateConfigurer
|
||||||
EnableRegistration bool
|
EnableRegistration bool
|
||||||
|
EnableClientRegistration bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type StateConfigurer interface {
|
type StateConfigurer interface {
|
||||||
|
@ -73,7 +74,8 @@ func (cfg *ServerConfig) Server() (*Server, error) {
|
||||||
HealthChecks: []health.Checkable{km},
|
HealthChecks: []health.Checkable{km},
|
||||||
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)
|
||||||
|
|
|
@ -29,19 +29,20 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
httpPathDiscovery = "/.well-known/openid-configuration"
|
httpPathDiscovery = "/.well-known/openid-configuration"
|
||||||
httpPathToken = "/token"
|
httpPathToken = "/token"
|
||||||
httpPathKeys = "/keys"
|
httpPathKeys = "/keys"
|
||||||
httpPathAuth = "/auth"
|
httpPathAuth = "/auth"
|
||||||
httpPathHealth = "/health"
|
httpPathHealth = "/health"
|
||||||
httpPathAPI = "/api"
|
httpPathAPI = "/api"
|
||||||
httpPathRegister = "/register"
|
httpPathRegister = "/register"
|
||||||
httpPathEmailVerify = "/verify-email"
|
httpPathEmailVerify = "/verify-email"
|
||||||
httpPathVerifyEmailResend = "/resend-verify-email"
|
httpPathVerifyEmailResend = "/resend-verify-email"
|
||||||
httpPathSendResetPassword = "/send-reset-password"
|
httpPathSendResetPassword = "/send-reset-password"
|
||||||
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"
|
||||||
|
|
|
@ -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 = ®Endpoint
|
||||||
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
|
Loading…
Reference in a new issue