Repository: SkygearIO/skygear-server
Pull request #1039 (2019-09-24 03:59:02)

Multi tenant azure AD
The code needed to change is actually very little but this PR fixes a couple of things.

1. Fix DenyDisabledUser
2. Store API Key in Oauth state so that it can be retrieved in the callback
3. Make email optional
4. Include `tenant` in Identity model
5. Return AuthResponse in link
6. Actually support two special tenant values

The feature PR: https://github.com/SkygearIO/features/pull/359



Patch:
diff --git a/pkg/auth/dependency/apiclientconfig/provider.go b/pkg/auth/dependency/apiclientconfig/provider.go
deleted file mode 100644
index 8f7cb0c8..00000000
--- a/pkg/auth/dependency/apiclientconfig/provider.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package apiclientconfig
-
-import (
-	"github.com/skygeario/skygear-server/pkg/core/config"
-)
-
-// Provider provides access to APIClientConfiguration.
-type Provider interface {
-	Get() (*config.APIClientConfiguration, bool)
-}
diff --git a/pkg/auth/dependency/apiclientconfig/provider_impl.go b/pkg/auth/dependency/apiclientconfig/provider_impl.go
deleted file mode 100644
index c7767d78..00000000
--- a/pkg/auth/dependency/apiclientconfig/provider_impl.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package apiclientconfig
-
-import (
-	coreAuth "github.com/skygeario/skygear-server/pkg/core/auth"
-	"github.com/skygeario/skygear-server/pkg/core/config"
-	"github.com/skygeario/skygear-server/pkg/core/model"
-)
-
-type providerImpl struct {
-	authContext coreAuth.ContextGetter
-	clients     map[string]config.APIClientConfiguration
-}
-
-func NewProvider(authContext coreAuth.ContextGetter, clients map[string]config.APIClientConfiguration) Provider {
-	return &providerImpl{
-		authContext: authContext,
-		clients:     clients,
-	}
-}
-
-func (p *providerImpl) Get() (*config.APIClientConfiguration, bool) {
-	accessKey := p.authContext.AccessKey()
-	if accessKey.ClientID == "" {
-		return nil, false
-	}
-	return model.GetClientConfig(p.clients, accessKey.ClientID)
-}
-
-var (
-	_ Provider = &providerImpl{}
-)
diff --git a/pkg/auth/dependency/principal/oauth/principal.go b/pkg/auth/dependency/principal/oauth/principal.go
index 53c878cb..92c7c814 100644
--- a/pkg/auth/dependency/principal/oauth/principal.go
+++ b/pkg/auth/dependency/principal/oauth/principal.go
@@ -47,9 +47,9 @@ func (p *Principal) ProviderID() string {
 }
 
 func (p *Principal) Attributes() principal.Attributes {
-	// TODO: promote tenant
 	return principal.Attributes{
 		"provider_type":    p.ProviderType,
+		"provider_keys":    p.ProviderKeys,
 		"provider_user_id": p.ProviderUserID,
 		"raw_profile":      p.UserProfile,
 	}
diff --git a/pkg/auth/dependency/sso/azureadv2.go b/pkg/auth/dependency/sso/azureadv2.go
index b1623565..48ddeb25 100644
--- a/pkg/auth/dependency/sso/azureadv2.go
+++ b/pkg/auth/dependency/sso/azureadv2.go
@@ -14,10 +14,6 @@ import (
 	"github.com/skygeario/skygear-server/pkg/core/crypto"
 )
 
-const (
-	azureadv2ConfigurationURLFormat string = "https://login.microsoftonline.com/%s/.well-known/openid-configuration"
-)
-
 type Azureadv2Impl struct {
 	OAuthConfig    config.OAuthConfiguration
 	ProviderConfig config.OAuthProviderConfiguration
@@ -32,7 +28,46 @@ type azureadv2OpenIDConfiguration struct {
 
 func (f *Azureadv2Impl) getOpenIDConfiguration() (c azureadv2OpenIDConfiguration, err error) {
 	// TODO(sso): Cache OpenID configuration
-	endpoint := fmt.Sprintf(azureadv2ConfigurationURLFormat, f.ProviderConfig.Tenant)
+
+	tenant := f.ProviderConfig.Tenant
+	var endpoint string
+	// GUIDE(sso): Azure special tenant
+	// If the azure tenant is `organizations` or `common`,
+	// the developer should make use of `before_user_create` and `before_identity_create` hook
+	// to disallow any undesire identity.
+	// The `raw_profile` of the identity is the ID Token claims.
+	// Refer to https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens
+	// to see what claims the token could contain.
+	//
+	// For `organizations`, the user can be any user of any organizational AD.
+	// Therefore the developer should have a whitelist of AD tenant IDs.
+	// In the incoming hook, check if `tid` matches one of the entry of the whitelist.
+	//
+	// For `common`, in addition to the users from `organizations`, any Microsoft personal account
+	// could be the user.
+	// In case of personal account, the `tid` is "9188040d-6c67-4c5b-b112-36a304b66dad".
+	// Therefore the developer should first check if `tid` indicates personal account.
+	// If yes, apply their logic to disallow the user creation.
+	// One very common example is to look at the claim `email`.
+	// Use a email address parser to parse the email address.
+	// Obtain the domain and check if the domain is whitelisted.
+	// For example, if the developer only wants user from hotmail.com to create user,
+	// ensure `tid` is "9188040d-6c67-4c5b-b112-36a304b66dad" and ensure `email`
+	// is of domain `@hotmail.com`.
+
+	// As of 2019-09-23, two special values are observed.
+	// To discover these values, create a new client
+	// and try different options.
+	switch tenant {
+	// Special value for any organizational AD
+	case "organizations":
+		endpoint = "https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration"
+	// Special value for any organizational AD and personal accounts (Xbox etc)
+	case "common":
+		endpoint = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration"
+	default:
+		endpoint = fmt.Sprintf("https://login.microsoftonline.com/%s/v2.0/.well-known/openid-configuration", tenant)
+	}
 
 	// nolint: gosec
 	resp, err := http.Get(endpoint)
@@ -171,11 +206,10 @@ func (f *Azureadv2Impl) OpenIDConnectGetAuthInfo(r OAuthAuthorizationResponse) (
 		err = fmt.Errorf("cannot find oid")
 		return
 	}
-	email, ok := mapClaims["email"].(string)
-	if !ok {
-		err = fmt.Errorf("cannot find email")
-		return
-	}
+	// For "Microsoft Account", email usually exists.
+	// For "AD guest user", email usually exists because to invite an user, the inviter must provide email.
+	// For "AD user", email never exists even one is provided in "Authentication Methods".
+	email, _ := mapClaims["email"].(string)
 
 	authInfo.ProviderConfig = f.ProviderConfig
 	authInfo.ProviderRawProfile = mapClaims
diff --git a/pkg/auth/dependency/sso/sso.go b/pkg/auth/dependency/sso/sso.go
index d945e314..49e5fbc6 100644
--- a/pkg/auth/dependency/sso/sso.go
+++ b/pkg/auth/dependency/sso/sso.go
@@ -38,7 +38,8 @@ type State struct {
 	LoginState
 	LinkState
 	OAuthAuthorizationCodeFlowState
-	Nonce string `json:"nonce,omitempty"`
+	Nonce       string `json:"nonce,omitempty"`
+	APIClientID string `json:"api_client_id"`
 }
 
 // UXMode indicates how the URL is used
diff --git a/pkg/auth/handler/mfa/authenticate_bearer_token.go b/pkg/auth/handler/mfa/authenticate_bearer_token.go
index c81b4df9..d8b7a70e 100644
--- a/pkg/auth/handler/mfa/authenticate_bearer_token.go
+++ b/pkg/auth/handler/mfa/authenticate_bearer_token.go
@@ -4,10 +4,10 @@ import (
 	"net/http"
 
 	"github.com/skygeario/skygear-server/pkg/auth"
-	"github.com/skygeario/skygear-server/pkg/auth/dependency/apiclientconfig"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/authnsession"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/hook"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/mfa"
+	"github.com/skygeario/skygear-server/pkg/core/apiclientconfig"
 	coreAuth "github.com/skygeario/skygear-server/pkg/core/auth"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz/policy"
@@ -107,7 +107,7 @@ func (h *AuthenticateBearerTokenHandler) DecodeRequest(request *http.Request) (h
 		return nil, err
 	}
 
-	apiClientConfig, ok := h.APIClientConfigurationProvider.Get()
+	_, apiClientConfig, ok := h.APIClientConfigurationProvider.Get()
 	if ok && apiClientConfig.SessionTransport == config.SessionTransportTypeCookie {
 		cookie, err := request.Cookie(coreHttp.CookieNameMFABearerToken)
 		if err == nil {
diff --git a/pkg/auth/handler/sso/auth_handler.go b/pkg/auth/handler/sso/auth_handler.go
index 89dbcb2e..57327d3f 100644
--- a/pkg/auth/handler/sso/auth_handler.go
+++ b/pkg/auth/handler/sso/auth_handler.go
@@ -17,6 +17,7 @@ import (
 	"github.com/skygeario/skygear-server/pkg/core/skyerr"
 
 	"github.com/skygeario/skygear-server/pkg/auth"
+	"github.com/skygeario/skygear-server/pkg/core/apiclientconfig"
 	"github.com/skygeario/skygear-server/pkg/core/async"
 	coreAuth "github.com/skygeario/skygear-server/pkg/core/auth"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
@@ -87,21 +88,23 @@ func (p AuthRequestPayload) Validate() error {
 // it will render a html page and set cookie in the response.
 //
 type AuthHandler struct {
-	TxContext               db.TxContext                `dependency:"TxContext"`
-	AuthContext             coreAuth.ContextGetter      `dependency:"AuthContextGetter"`
-	OAuthAuthProvider       oauth.Provider              `dependency:"OAuthAuthProvider"`
-	IdentityProvider        principal.IdentityProvider  `dependency:"IdentityProvider"`
-	AuthInfoStore           authinfo.Store              `dependency:"AuthInfoStore"`
-	AuthnSessionProvider    authnsession.Provider       `dependency:"AuthnSessionProvider"`
-	AuthHandlerHTMLProvider sso.AuthHandlerHTMLProvider `dependency:"AuthHandlerHTMLProvider"`
-	ProviderFactory         *sso.ProviderFactory        `dependency:"SSOProviderFactory"`
-	UserProfileStore        userprofile.Store           `dependency:"UserProfileStore"`
-	HookProvider            hook.Provider               `dependency:"HookProvider"`
-	OAuthConfiguration      config.OAuthConfiguration   `dependency:"OAuthConfiguration"`
-	WelcomeEmailEnabled     bool                        `dependency:"WelcomeEmailEnabled"`
-	TaskQueue               async.Queue                 `dependency:"AsyncTaskQueue"`
-	Provider                sso.OAuthProvider
-	ProviderID              string
+	TxContext                      db.TxContext                `dependency:"TxContext"`
+	AuthContext                    coreAuth.ContextGetter      `dependency:"AuthContextGetter"`
+	AuthContextSetter              coreAuth.ContextSetter      `dependency:"AuthContextSetter"`
+	APIClientConfigurationProvider apiclientconfig.Provider    `dependency:"APIClientConfigurationProvider"`
+	OAuthAuthProvider              oauth.Provider              `dependency:"OAuthAuthProvider"`
+	IdentityProvider               principal.IdentityProvider  `dependency:"IdentityProvider"`
+	AuthInfoStore                  authinfo.Store              `dependency:"AuthInfoStore"`
+	AuthnSessionProvider           authnsession.Provider       `dependency:"AuthnSessionProvider"`
+	AuthHandlerHTMLProvider        sso.AuthHandlerHTMLProvider `dependency:"AuthHandlerHTMLProvider"`
+	ProviderFactory                *sso.ProviderFactory        `dependency:"SSOProviderFactory"`
+	UserProfileStore               userprofile.Store           `dependency:"UserProfileStore"`
+	HookProvider                   hook.Provider               `dependency:"HookProvider"`
+	OAuthConfiguration             config.OAuthConfiguration   `dependency:"OAuthConfiguration"`
+	WelcomeEmailEnabled            bool                        `dependency:"WelcomeEmailEnabled"`
+	TaskQueue                      async.Queue                 `dependency:"AsyncTaskQueue"`
+	Provider                       sso.OAuthProvider
+	ProviderID                     string
 }
 
 func (h AuthHandler) DecodeRequest(request *http.Request) (handler.RequestPayload, error) {
@@ -173,6 +176,10 @@ func (h AuthHandler) Handle(w http.ResponseWriter, r *http.Request) (success boo
 		return
 	}
 
+	// Extract API Key from state
+	key := h.APIClientConfigurationProvider.GetAccessKeyByClientID(state.APIClientID)
+	h.AuthContextSetter.SetAccessKey(key)
+
 	if err = h.validateCallbackURL(h.OAuthConfiguration.AllowedCallbackURLs, state.CallbackURL); err != nil {
 		http.Error(w, err.Error(), http.StatusBadRequest)
 		return
diff --git a/pkg/auth/handler/sso/auth_url.go b/pkg/auth/handler/sso/auth_url.go
index 1508148c..e22e5328 100644
--- a/pkg/auth/handler/sso/auth_url.go
+++ b/pkg/auth/handler/sso/auth_url.go
@@ -4,12 +4,12 @@ import (
 	"net/http"
 
 	"github.com/gorilla/mux"
-	"github.com/skygeario/skygear-server/pkg/auth/dependency/sso"
-	"github.com/skygeario/skygear-server/pkg/core/skyerr"
 
 	"github.com/skygeario/skygear-server/pkg/auth"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/principal/password"
+	"github.com/skygeario/skygear-server/pkg/auth/dependency/sso"
 	"github.com/skygeario/skygear-server/pkg/auth/model"
+	"github.com/skygeario/skygear-server/pkg/core/apiclientconfig"
 	coreAuth "github.com/skygeario/skygear-server/pkg/core/auth"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authz/policy"
@@ -20,6 +20,7 @@ import (
 	coreHttp "github.com/skygeario/skygear-server/pkg/core/http"
 	"github.com/skygeario/skygear-server/pkg/core/inject"
 	"github.com/skygeario/skygear-server/pkg/core/server"
+	"github.com/skygeario/skygear-server/pkg/core/skyerr"
 )
 
 func AttachAuthURLHandler(
@@ -232,14 +233,15 @@ func (p AuthURLRequestPayload) Validate() (err error) {
 		@Callback user_sync {UserSyncEvent}
 */
 type AuthURLHandler struct {
-	TxContext            db.TxContext              `dependency:"TxContext"`
-	AuthContext          coreAuth.ContextGetter    `dependency:"AuthContextGetter"`
-	ProviderFactory      *sso.ProviderFactory      `dependency:"SSOProviderFactory"`
-	PasswordAuthProvider password.Provider         `dependency:"PasswordAuthProvider"`
-	OAuthConfiguration   config.OAuthConfiguration `dependency:"OAuthConfiguration"`
-	Provider             sso.OAuthProvider
-	ProviderID           string
-	Action               string
+	TxContext                      db.TxContext              `dependency:"TxContext"`
+	AuthContext                    coreAuth.ContextGetter    `dependency:"AuthContextGetter"`
+	APIClientConfigurationProvider apiclientconfig.Provider  `dependency:"APIClientConfigurationProvider"`
+	ProviderFactory                *sso.ProviderFactory      `dependency:"SSOProviderFactory"`
+	PasswordAuthProvider           password.Provider         `dependency:"PasswordAuthProvider"`
+	OAuthConfiguration             config.OAuthConfiguration `dependency:"OAuthConfiguration"`
+	Provider                       sso.OAuthProvider
+	ProviderID                     string
+	Action                         string
 }
 
 func (h *AuthURLHandler) ProvideAuthzPolicy() authz.Policy {
@@ -333,6 +335,8 @@ func (h *AuthURLHandler) Handle(w http.ResponseWriter, r *http.Request) (result
 	}
 	http.SetCookie(w, cookie)
 
+	apiClientID, _, _ := h.APIClientConfigurationProvider.Get()
+
 	params := sso.GetURLParams{
 		State: sso.State{
 			LoginState: sso.LoginState{
@@ -344,7 +348,8 @@ func (h *AuthURLHandler) Handle(w http.ResponseWriter, r *http.Request) (result
 				UXMode:      payload.UXMode,
 				Action:      h.Action,
 			},
-			Nonce: crypto.SHA256String(nonce),
+			Nonce:       crypto.SHA256String(nonce),
+			APIClientID: apiClientID,
 		},
 	}
 	authInfo, _ := h.AuthContext.AuthInfo()
diff --git a/pkg/auth/handler/sso/sso.go b/pkg/auth/handler/sso/sso.go
index 452e22ca..0eda10bb 100644
--- a/pkg/auth/handler/sso/sso.go
+++ b/pkg/auth/handler/sso/sso.go
@@ -160,7 +160,7 @@ func (h respHandler) linkActionResp(oauthAuthInfo sso.AuthInfo, linkState sso.Li
 		return
 	}
 
-	resp = map[string]string{}
+	resp = model.NewAuthResponseWithUser(user)
 	return
 }
 
diff --git a/pkg/auth/inject.go b/pkg/auth/inject.go
index 5e8f67be..b8657522 100644
--- a/pkg/auth/inject.go
+++ b/pkg/auth/inject.go
@@ -7,7 +7,6 @@ import (
 	"github.com/go-gomail/gomail"
 	"github.com/sirupsen/logrus"
 
-	"github.com/skygeario/skygear-server/pkg/auth/dependency/apiclientconfig"
 	authAudit "github.com/skygeario/skygear-server/pkg/auth/dependency/audit"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/authnsession"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/forgotpwdemail"
@@ -25,6 +24,7 @@ import (
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/userverify"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/welcemail"
 	authTemplate "github.com/skygeario/skygear-server/pkg/auth/template"
+	"github.com/skygeario/skygear-server/pkg/core/apiclientconfig"
 	"github.com/skygeario/skygear-server/pkg/core/async"
 	"github.com/skygeario/skygear-server/pkg/core/audit"
 	coreAuth "github.com/skygeario/skygear-server/pkg/core/auth"
@@ -352,7 +352,7 @@ func (m DependencyMap) Provide(
 	case "MFAConfiguration":
 		return tConfig.UserConfig.MFA
 	case "APIClientConfigurationProvider":
-		return apiclientconfig.NewProvider(newAuthContext(), tConfig.UserConfig.Clients)
+		return apiclientconfig.NewProvider(newAuthContext(), tConfig)
 	default:
 		return nil
 	}
diff --git a/pkg/core/apiclientconfig/mock_provider.go b/pkg/core/apiclientconfig/mock_provider.go
new file mode 100644
index 00000000..29bba165
--- /dev/null
+++ b/pkg/core/apiclientconfig/mock_provider.go
@@ -0,0 +1,47 @@
+package apiclientconfig
+
+import (
+	"github.com/skygeario/skygear-server/pkg/core/config"
+	"github.com/skygeario/skygear-server/pkg/core/model"
+)
+
+type MockProvider struct {
+	ClientID        string
+	APIClientConfig *config.APIClientConfiguration
+}
+
+func NewMockProvider(clientID string) *MockProvider {
+	return &MockProvider{
+		ClientID: clientID,
+		APIClientConfig: &config.APIClientConfiguration{
+			Name:                 clientID,
+			APIKey:               clientID,
+			SessionTransport:     config.SessionTransportTypeHeader,
+			AccessTokenLifetime:  1800,
+			RefreshTokenLifetime: 86400,
+			SameSite:             config.SessionCookieSameSiteLax,
+		},
+	}
+}
+
+func (p *MockProvider) Get() (string, *config.APIClientConfiguration, bool) {
+	if p.APIClientConfig == nil {
+		return "", nil, false
+	}
+	return p.ClientID, p.APIClientConfig, true
+}
+
+func (p *MockProvider) GetAccessKeyByAPIKey(apiKey string) model.AccessKey {
+	if p.APIClientConfig == nil {
+		return model.AccessKey{Type: model.NoAccessKeyType}
+	}
+	return model.AccessKey{Type: model.APIAccessKeyType, ClientID: p.ClientID}
+}
+
+func (p *MockProvider) GetAccessKeyByClientID(clientID string) model.AccessKey {
+	return model.AccessKey{Type: model.APIAccessKeyType, ClientID: clientID}
+}
+
+var (
+	_ Provider = &MockProvider{}
+)
diff --git a/pkg/core/apiclientconfig/provider.go b/pkg/core/apiclientconfig/provider.go
new file mode 100644
index 00000000..ca89727b
--- /dev/null
+++ b/pkg/core/apiclientconfig/provider.go
@@ -0,0 +1,13 @@
+package apiclientconfig
+
+import (
+	"github.com/skygeario/skygear-server/pkg/core/config"
+	"github.com/skygeario/skygear-server/pkg/core/model"
+)
+
+// Provider provides access to APIClientConfiguration.
+type Provider interface {
+	Get() (string, *config.APIClientConfiguration, bool)
+	GetAccessKeyByAPIKey(apiKey string) model.AccessKey
+	GetAccessKeyByClientID(clientID string) model.AccessKey
+}
diff --git a/pkg/core/apiclientconfig/provider_impl.go b/pkg/core/apiclientconfig/provider_impl.go
new file mode 100644
index 00000000..dda335fe
--- /dev/null
+++ b/pkg/core/apiclientconfig/provider_impl.go
@@ -0,0 +1,56 @@
+package apiclientconfig
+
+import (
+	"crypto/subtle"
+
+	coreAuth "github.com/skygeario/skygear-server/pkg/core/auth"
+	"github.com/skygeario/skygear-server/pkg/core/config"
+	"github.com/skygeario/skygear-server/pkg/core/model"
+)
+
+type providerImpl struct {
+	authContext  coreAuth.ContextGetter
+	tenantConfig config.TenantConfiguration
+}
+
+func NewProvider(authContext coreAuth.ContextGetter, tenantConfig config.TenantConfiguration) Provider {
+	return &providerImpl{
+		authContext:  authContext,
+		tenantConfig: tenantConfig,
+	}
+}
+
+func (p *providerImpl) Get() (string, *config.APIClientConfiguration, bool) {
+	accessKey := p.authContext.AccessKey()
+	if accessKey.ClientID == "" {
+		return "", nil, false
+	}
+	clientID := accessKey.ClientID
+	c, ok := model.GetClientConfig(p.tenantConfig.UserConfig.Clients, clientID)
+	return clientID, c, ok
+}
+
+func (p *providerImpl) GetAccessKeyByAPIKey(apiKey string) model.AccessKey {
+	if subtle.ConstantTimeCompare([]byte(apiKey), []byte(p.tenantConfig.UserConfig.MasterKey)) == 1 {
+		return model.AccessKey{Type: model.MasterAccessKeyType}
+	}
+
+	for id, clientConfig := range p.tenantConfig.UserConfig.Clients {
+		if clientConfig.Disabled {
+			continue
+		}
+		if subtle.ConstantTimeCompare([]byte(apiKey), []byte(clientConfig.APIKey)) == 1 {
+			return model.AccessKey{Type: model.APIAccessKeyType, ClientID: id}
+		}
+	}
+
+	return model.AccessKey{Type: model.NoAccessKeyType}
+}
+
+func (p *providerImpl) GetAccessKeyByClientID(clientID string) model.AccessKey {
+	return model.AccessKey{Type: model.APIAccessKeyType, ClientID: clientID}
+}
+
+var (
+	_ Provider = &providerImpl{}
+)
diff --git a/pkg/core/auth/authz/policy/disable.go b/pkg/core/auth/authz/policy/disable.go
index 2b69dcc9..73196234 100644
--- a/pkg/core/auth/authz/policy/disable.go
+++ b/pkg/core/auth/authz/policy/disable.go
@@ -8,18 +8,14 @@ import (
 	"github.com/skygeario/skygear-server/pkg/core/skyerr"
 )
 
+// DenyDisabledUser denies disabled user.
+// It is not an error if the request does not have an associated user.
+// If you want to enforce enabled user, use both RequireAuthenticated and DenyDisabledUser.
 func DenyDisabledUser(r *http.Request, ctx auth.ContextGetter) error {
 	authInfo, _ := ctx.AuthInfo()
-	if authInfo == nil {
-		return skyerr.NewError(skyerr.UnexpectedAuthInfoNotFound, "user authentication info not found")
+	if authInfo != nil && authInfo.Disabled {
+		return skyerr.NewError(skyerr.UserDisabled, "user disabled")
 	}
-
-	if authInfo.Disabled {
-		// TODO:
-		// return proper error code
-		return skyerr.NewError(skyerr.UnexpectedError, "user disabled")
-	}
-
 	return nil
 }
 
diff --git a/pkg/core/middleware/authn.go b/pkg/core/middleware/authn.go
index 93e19232..1c9237ca 100644
--- a/pkg/core/middleware/authn.go
+++ b/pkg/core/middleware/authn.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/skygeario/skygear-server/pkg/core/logging"
 
+	"github.com/skygeario/skygear-server/pkg/core/apiclientconfig"
 	"github.com/skygeario/skygear-server/pkg/core/auth"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
 	"github.com/skygeario/skygear-server/pkg/core/auth/session"
@@ -16,12 +17,13 @@ import (
 
 // AuthnMiddleware populate auth context information
 type AuthnMiddleware struct {
-	AuthContextSetter auth.ContextSetter `dependency:"AuthContextSetter"`
-	LoggerFactory     logging.Factory    `dependency:"LoggerFactory"`
-	SessionProvider   session.Provider   `dependency:"SessionProvider"`
-	SessionWriter     session.Writer     `dependency:"SessionWriter"`
-	AuthInfoStore     authinfo.Store     `dependency:"AuthInfoStore"`
-	TxContext         db.TxContext       `dependency:"TxContext"`
+	APIClientConfigurationProvider apiclientconfig.Provider `dependency:"APIClientConfigurationProvider"`
+	AuthContextSetter              auth.ContextSetter       `dependency:"AuthContextSetter"`
+	LoggerFactory                  logging.Factory          `dependency:"LoggerFactory"`
+	SessionProvider                session.Provider         `dependency:"SessionProvider"`
+	SessionWriter                  session.Writer           `dependency:"SessionWriter"`
+	AuthInfoStore                  authinfo.Store           `dependency:"AuthInfoStore"`
+	TxContext                      db.TxContext             `dependency:"TxContext"`
 }
 
 // AuthnMiddlewareFactory creates AuthnMiddleware per request.
@@ -59,7 +61,7 @@ func (m *AuthnMiddleware) Handle(next http.Handler) http.Handler {
 
 		tenantConfig := config.GetTenantConfig(r)
 
-		key := model.CheckAccessKey(tenantConfig, model.GetAPIKey(r))
+		key := m.APIClientConfigurationProvider.GetAccessKeyByAPIKey(model.GetAPIKey(r))
 		m.AuthContextSetter.SetAccessKey(key)
 
 		accessToken, transport, err := model.GetAccessToken(r)
diff --git a/pkg/core/model/app.go b/pkg/core/model/app.go
index 5c6bd3d6..99e34aaf 100644
--- a/pkg/core/model/app.go
+++ b/pkg/core/model/app.go
@@ -1,7 +1,6 @@
 package model
 
 import (
-	"crypto/subtle"
 	"fmt"
 	"net/http"
 	"strings"
@@ -74,23 +73,6 @@ func GetAPIKey(i interface{}) string {
 	return header(i).Get(coreHttp.HeaderAPIKey)
 }
 
-func CheckAccessKey(config config.TenantConfiguration, apiKey string) AccessKey {
-	if subtle.ConstantTimeCompare([]byte(apiKey), []byte(config.UserConfig.MasterKey)) == 1 {
-		return AccessKey{Type: MasterAccessKeyType}
-	}
-
-	for id, clientConfig := range config.UserConfig.Clients {
-		if clientConfig.Disabled {
-			continue
-		}
-		if subtle.ConstantTimeCompare([]byte(apiKey), []byte(clientConfig.APIKey)) == 1 {
-			return AccessKey{Type: APIAccessKeyType, ClientID: id}
-		}
-	}
-
-	return AccessKey{Type: NoAccessKeyType}
-}
-
 func GetClientConfig(c map[string]config.APIClientConfiguration, clientID string) (*config.APIClientConfiguration, bool) {
 	for id, clientConfig := range c {
 		if id == clientID {
diff --git a/pkg/core/skyerr/errorcode_string.go b/pkg/core/skyerr/errorcode_string.go
index d89a8594..c2470561 100644
--- a/pkg/core/skyerr/errorcode_string.go
+++ b/pkg/core/skyerr/errorcode_string.go
@@ -28,17 +28,15 @@ func _() {
 	_ = x[InvalidAuthenticationSession-118]
 	_ = x[InvalidMFABearerToken-119]
 	_ = x[UnexpectedError-10000]
-	_ = x[UnexpectedAuthInfoNotFound-10001]
 }
 
 const (
 	_ErrorCode_name_0 = "NotAuthenticatedPermissionDeniedAccessKeyNotAcceptedAccessTokenNotAcceptedInvalidCredentialsBadRequestInvalidArgumentDuplicatedResourceNotFoundUndefinedOperationPasswordPolicyViolatedUserDisabledVerificationRequiredWebHookTimeOutWebHookFailedCurrentIdentityBeingDeletedAuthenticationSessionInvalidAuthenticationSessionInvalidMFABearerToken"
-	_ErrorCode_name_1 = "UnexpectedErrorUnexpectedAuthInfoNotFound"
+	_ErrorCode_name_1 = "UnexpectedError"
 )
 
 var (
 	_ErrorCode_index_0 = [...]uint16{0, 16, 32, 52, 74, 92, 102, 117, 127, 143, 161, 183, 195, 215, 229, 242, 269, 290, 318, 339}
-	_ErrorCode_index_1 = [...]uint8{0, 15, 41}
 )
 
 func (i ErrorCode) String() string {
@@ -46,9 +44,8 @@ func (i ErrorCode) String() string {
 	case 101 <= i && i <= 119:
 		i -= 101
 		return _ErrorCode_name_0[_ErrorCode_index_0[i]:_ErrorCode_index_0[i+1]]
-	case 10000 <= i && i <= 10001:
-		i -= 10000
-		return _ErrorCode_name_1[_ErrorCode_index_1[i]:_ErrorCode_index_1[i+1]]
+	case i == 10000:
+		return _ErrorCode_name_1
 	default:
 		return "ErrorCode(" + strconv.FormatInt(int64(i), 10) + ")"
 	}
diff --git a/pkg/core/skyerr/skyerr.go b/pkg/core/skyerr/skyerr.go
index b8cbf152..8c7174bc 100644
--- a/pkg/core/skyerr/skyerr.go
+++ b/pkg/core/skyerr/skyerr.go
@@ -120,7 +120,6 @@ const (
 	//
 	// Refrain from using this error code.
 	UnexpectedError ErrorCode = 10000 + iota
-	UnexpectedAuthInfoNotFound
 
 	// Error codes for unexpected error condition should be placed
 	// above this line.


Test patch:
diff --git a/pkg/auth/handler/sso/auth_handler_test.go b/pkg/auth/handler/sso/auth_handler_test.go
index a13e63a8..10b32073 100644
--- a/pkg/auth/handler/sso/auth_handler_test.go
+++ b/pkg/auth/handler/sso/auth_handler_test.go
@@ -20,6 +20,7 @@ import (
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/sso"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/userprofile"
 	"github.com/skygeario/skygear-server/pkg/auth/model"
+	"github.com/skygeario/skygear-server/pkg/core/apiclientconfig"
 	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
 	"github.com/skygeario/skygear-server/pkg/core/auth/metadata"
 	"github.com/skygeario/skygear-server/pkg/core/auth/session"
@@ -94,9 +95,12 @@ func TestAuthHandler(t *testing.T) {
 		providerUserID := "mock_user_id"
 		sh := &AuthHandler{}
 		sh.TxContext = db.NewMockTxContext()
-		sh.AuthContext = authtest.NewMockContext().
+		sh.APIClientConfigurationProvider = apiclientconfig.NewMockProvider("api_key")
+		authContext := authtest.NewMockContext().
 			UseUser("faseng.cat.id", "faseng.cat.principal.id").
 			MarkVerified()
+		sh.AuthContext = authContext
+		sh.AuthContextSetter = authContext
 		oauthConfig := coreconfig.OAuthConfiguration{
 			URLPrefix:      "http://localhost:3000",
 			StateJWTSecret: stateJWTSecret,
@@ -231,6 +235,7 @@ func TestAuthHandler(t *testing.T) {
 						"identity": {
 							"id": "%s",
 							"type": "oauth",
+							"provider_keys": {},
 							"provider_type": "google",
 							"provider_user_id": "mock_user_id",
 							"raw_profile": {
@@ -333,6 +338,7 @@ func TestAuthHandler(t *testing.T) {
 					"identity": {
 						"id": "%s",
 						"type": "oauth",
+						"provider_keys": {},
 						"provider_type": "google",
 						"provider_user_id": "mock_user_id",
 						"raw_profile": {
@@ -354,10 +360,13 @@ func TestAuthHandler(t *testing.T) {
 		action := "link"
 		stateJWTSecret := "secret"
 		sh := &AuthHandler{}
+		sh.APIClientConfigurationProvider = apiclientconfig.NewMockProvider("api_key")
 		sh.TxContext = db.NewMockTxContext()
-		sh.AuthContext = authtest.NewMockContext().
+		authContext := authtest.NewMockContext().
 			UseUser("faseng.cat.id", "faseng.cat.principal.id").
 			MarkVerified()
+		sh.AuthContext = authContext
+		sh.AuthContextSetter = authContext
 		oauthConfig := coreconfig.OAuthConfiguration{
 			URLPrefix:      "http://localhost:3000",
 			StateJWTSecret: stateJWTSecret,
@@ -488,7 +497,16 @@ func TestAuthHandler(t *testing.T) {
 			{
 				"callback_url": "http://localhost:3000",
 				"result": {
-					"result": {}
+					"result": {
+						"user": {
+							"id": "john.doe.id",
+							"is_verified": false,
+							"is_disabled": false,
+							"created_at": "0001-01-01T00:00:00Z",
+							"verify_info": {},
+							"metadata": {}
+						}
+					}
 				}
 			}
 			`)
@@ -560,10 +578,13 @@ func TestAuthHandler(t *testing.T) {
 		providerUserID := "mock_user_id"
 
 		sh := &AuthHandler{}
+		sh.APIClientConfigurationProvider = apiclientconfig.NewMockProvider("api_key")
 		sh.TxContext = db.NewMockTxContext()
-		sh.AuthContext = authtest.NewMockContext().
+		authContext := authtest.NewMockContext().
 			UseUser("faseng.cat.id", "faseng.cat.principal.id").
 			MarkVerified()
+		sh.AuthContext = authContext
+		sh.AuthContextSetter = authContext
 		oauthConfig := coreconfig.OAuthConfiguration{
 			URLPrefix:      "http://localhost:3000",
 			StateJWTSecret: stateJWTSecret,
@@ -759,6 +780,7 @@ func TestAuthHandler(t *testing.T) {
 						"identity": {
 							"type": "oauth",
 							"id": "%s",
+							"provider_keys": {},
 							"provider_type": "google",
 							"provider_user_id": "%s",
 							"raw_profile": {
@@ -836,6 +858,7 @@ func TestAuthHandler(t *testing.T) {
 						"identity": {
 							"type": "oauth",
 							"id": "%s",
+							"provider_keys": {},
 							"provider_type": "google",
 							"provider_user_id": "%s",
 							"raw_profile": {
diff --git a/pkg/auth/handler/sso/auth_url_test.go b/pkg/auth/handler/sso/auth_url_test.go
index 94781814..843021bc 100644
--- a/pkg/auth/handler/sso/auth_url_test.go
+++ b/pkg/auth/handler/sso/auth_url_test.go
@@ -16,6 +16,7 @@ import (
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/principal/password"
 	"github.com/skygeario/skygear-server/pkg/auth/dependency/sso"
 	"github.com/skygeario/skygear-server/pkg/auth/model"
+	"github.com/skygeario/skygear-server/pkg/core/apiclientconfig"
 	coreconfig "github.com/skygeario/skygear-server/pkg/core/config"
 	"github.com/skygeario/skygear-server/pkg/core/db"
 	"github.com/skygeario/skygear-server/pkg/core/skyerr"
@@ -70,6 +71,7 @@ func TestAuthURLPayload(t *testing.T) {
 func TestAuthURLHandler(t *testing.T) {
 	Convey("Test TestAuthURLHandler", t, func() {
 		h := &AuthURLHandler{}
+		h.APIClientConfigurationProvider = apiclientconfig.NewMockProvider("api_key")
 		h.AuthContext = authtest.NewMockContext().
 			UseUser("faseng.cat.id", "faseng.cat.principal.id").
 			MarkVerified()
diff --git a/pkg/auth/handler/sso/link_test.go b/pkg/auth/handler/sso/link_test.go
index 4af0dd63..5648c400 100644
--- a/pkg/auth/handler/sso/link_test.go
+++ b/pkg/auth/handler/sso/link_test.go
@@ -111,7 +111,16 @@ func TestLinkHandler(t *testing.T) {
 			h.ServeHTTP(resp, req)
 			So(resp.Code, ShouldEqual, 200)
 			So(resp.Body.Bytes(), ShouldEqualJSON, `{
-				"result": {}
+				"result": {
+					"user": {
+						"id": "faseng.cat.id",
+						"created_at": "0001-01-01T00:00:00Z",
+						"is_disabled": false,
+						"is_verified": false,
+						"metadata": {},
+						"verify_info": {}
+					}
+				}
 			}`)
 
 			p, _ := sh.OAuthAuthProvider.GetPrincipalByProvider(oauth.GetByProviderOptions{
@@ -131,6 +140,7 @@ func TestLinkHandler(t *testing.T) {
 						ID:   p.ID,
 						Type: "oauth",
 						Attributes: principal.Attributes{
+							"provider_keys":    map[string]interface{}{},
 							"provider_type":    "google",
 							"provider_user_id": "mock_user_id",
 							"raw_profile": map[string]interface{}{
diff --git a/pkg/auth/handler/sso/login_test.go b/pkg/auth/handler/sso/login_test.go
index eb6f80a8..ddfae39a 100644
--- a/pkg/auth/handler/sso/login_test.go
+++ b/pkg/auth/handler/sso/login_test.go
@@ -152,6 +152,7 @@ func TestLoginHandler(t *testing.T) {
 					"identity": {
 						"id": "%s",
 						"type": "oauth",
+						"provider_keys": {},
 						"provider_type": "google",
 						"provider_user_id": "mock_user_id",
 						"raw_profile": {
@@ -180,6 +181,7 @@ func TestLoginHandler(t *testing.T) {
 							ID:   p.ID,
 							Type: "oauth",
 							Attributes: principal.Attributes{
+								"provider_keys":    map[string]interface{}{},
 								"provider_type":    "google",
 								"provider_user_id": "mock_user_id",
 								"raw_profile": map[string]interface{}{
@@ -205,6 +207,7 @@ func TestLoginHandler(t *testing.T) {
 						ID:   p.ID,
 						Type: "oauth",
 						Attributes: principal.Attributes{
+							"provider_keys":    map[string]interface{}{},
 							"provider_type":    "google",
 							"provider_user_id": "mock_user_id",
 							"raw_profile": map[string]interface{}{
diff --git a/pkg/auth/handler/sso/unlink_test.go b/pkg/auth/handler/sso/unlink_test.go
index 55483743..2545883c 100644
--- a/pkg/auth/handler/sso/unlink_test.go
+++ b/pkg/auth/handler/sso/unlink_test.go
@@ -114,6 +114,7 @@ func TestUnlinkHandler(t *testing.T) {
 						ID:   "oauth-principal-id",
 						Type: "oauth",
 						Attributes: principal.Attributes{
+							"provider_keys":    map[string]interface{}{},
 							"provider_type":    "google",
 							"provider_user_id": "mock_user_id",
 							"raw_profile":      nil,
diff --git a/pkg/core/auth/authz/policy/disable_test.go b/pkg/core/auth/authz/policy/disable_test.go
index aaa2116c..8103a337 100644
--- a/pkg/core/auth/authz/policy/disable_test.go
+++ b/pkg/core/auth/authz/policy/disable_test.go
@@ -11,12 +11,12 @@ import (
 
 func TestDenyDisabledUser(t *testing.T) {
 	Convey("Test DenyDisabledUser", t, func() {
-		Convey("should return error if auth context has no auth info", func() {
+		Convey("should not return error if auth context has no auth info", func() {
 			req, _ := http.NewRequest("POST", "/", nil)
 			ctx := MemoryContextGetter{}
 
 			err := DenyDisabledUser(req, ctx)
-			So(err, ShouldNotBeEmpty)
+			So(err, ShouldBeNil)
 		})
 
 		Convey("should return error if user is disabled", func() {
@@ -29,7 +29,7 @@ func TestDenyDisabledUser(t *testing.T) {
 			}
 
 			err := DenyDisabledUser(req, ctx)
-			So(err, ShouldNotBeEmpty)
+			So(err, ShouldNotBeNil)
 		})
 
 		Convey("should pass if user is not disabled", func() {
@@ -42,7 +42,7 @@ func TestDenyDisabledUser(t *testing.T) {
 			}
 
 			err := DenyDisabledUser(req, ctx)
-			So(err, ShouldBeEmpty)
+			So(err, ShouldBeNil)
 		})
 
 	})
diff --git a/pkg/core/auth/testing/context.go b/pkg/core/auth/testing/context.go
index c73628ba..4caf6067 100644
--- a/pkg/core/auth/testing/context.go
+++ b/pkg/core/auth/testing/context.go
@@ -15,7 +15,10 @@ type MockContext struct {
 	err       error
 }
 
-var _ auth.ContextGetter = &MockContext{}
+var (
+	_ auth.ContextGetter = &MockContext{}
+	_ auth.ContextSetter = &MockContext{}
+)
 
 func NewMockContext() *MockContext {
 	return &MockContext{accessKey: model.AccessKey{Type: model.APIAccessKeyType}}
@@ -33,6 +36,16 @@ func (m *MockContext) Session() (*auth.Session, error) {
 	return m.session, m.err
 }
 
+func (m *MockContext) SetAccessKey(key model.AccessKey) {
+	m.accessKey = key
+}
+
+func (m *MockContext) SetSessionAndAuthInfo(sess *auth.Session, info *authinfo.AuthInfo, err error) {
+	m.session = sess
+	m.authInfo = info
+	m.err = err
+}
+
 func (m *MockContext) UseAPIAccessKey(clientID string) *MockContext {
 	m.accessKey.Type = model.APIAccessKeyType
 	m.accessKey.ClientID = clientID