dex-身份认证连接器

dex

一、简介
   开源项目dex,一个基于OpenID Connect的身份服务组件。Dex是一种身份服务,使用OpenID Connect来驱动其它应用程序的身份验证。分为dex-server和dex-client。Dex通过“连接器” portal充当其他身份提供者的门户。 这使dex可以将身份验证推送到LDAP服务器,SAML提供程序或已建立的身份提供程序(如GitHub,Google和Active Directory)的身份验证。 客户编写一次身份验证逻辑以与dex进行对话,然后dex处理给定后端的协议。但是目前dex不支持用户管理,在v2.md的文件中可以发现dex的开发人员计划在后续版本中添加用户管理的功能。要实现用户的开户销户等增删改查的工作,可能需要client-app直接对接Upstream Idp。

dex-身份认证连接器_第1张图片

​   当用户通过dex登录时,该用户的身份通常存储在另一个用户管理系统中:LDAP目录,GitHub组织等。Dex充当客户端应用程序和上游身份提供者之间的中介。 客户端只需要了解OpenID Connect即可查询dex,而dex实现了一系列用于查询其他用户管理系统的协议。“连接器”是dex用于根据一个身份提供者对用户进行身份验证的策略。 Dex实现了针对特定平台(例如GitHub,LinkedIn和Microsoft)以及已建立的协议(例如LDAP和SAML)的连接器。

二、 dex-storage:

Dex requires persisting state to perform various tasks such as track refresh tokens, preventing replays, and rotating keys. Storage breaches are serious as they can affect applications that rely on dex. Dex saves sensitive data in its backing storage, including signing keys and bcrypt’d passwords. As such, transport security and database ACLs should both be used, no matter which storage option is chosen.

​   dex-storage的作用: 安全起见,dexserver签发的id_token有效期通常不会太长,这就需要dexclient凭借Token中的refresh_token隔段时间重新换取新的Token,并通过某种机制将新Token中的id_token重新发回浏览器端保存。以refresh_token重新换取新的Token。dex需要持久化保存数据来执行各种各样的任务例如track refresh tokens、preventing replays、and rotating keys。dexserver在运行时跟踪refresh_tokenauth_codekeyspassword等,还要存储connectors、认证请求等信息,因此需要将这些状态保存下来。并且storage也提供了对存储数据增删改查的接口。dex提供了多种存储方案,如etcdCRDsSQLite3PostgresMySQLmemory

​   dexserver会根据项目情况配置一个合适的Storage,用以安全可靠地保存refresh_tokenauth_codekeyspassword等的状态。要考虑这个Storage实现方案的性能、稳定性、高可用性等多个因素。storage段的配置的是dexserver的配置文件中进行存设置。

//connector示例
type Connector struct {
	// ID that will uniquely identify the connector object.
	ID string `json:"id"`
	// The Type of the connector. E.g. 'oidc' or 'ldap'
	Type string `json:"type"`
	// The Name of the connector that is used when displaying it to the end user.
	Name string `json:"name"`
	// ResourceVersion is the static versioning used to keep track of dynamic configuration
	// changes to the connector object made by the API calls.
	ResourceVersion string `json:"resourceVersion"`
	// Config holds all the configuration information specific to the connector type. Since there
	// no generic struct we can use for this purpose, it is stored as a byte stream.
	Config []byte `json:"email"`
}

func (c *conn) CreateConnector(connector storage.Connector) error {
	_, err := c.Exec(`
		insert into connector (
			id, type, name, resource_version, config
		)
		values (
			$1, $2, $3, $4, $5
		);
	`,
		connector.ID, connector.Type, connector.Name, connector.ResourceVersion, connector.Config,
                    )
}

并且dex-storage存储connectors等数据之外,还有存储认证请求的接口。也就是dex-server每向后端认证一次,该认证请求会备份到dex-storage中。

func (c *conn) CreateAuthRequest(a storage.AuthRequest) error {
	_, err := c.Exec(`
		insert into auth_request (
			id, client_id, response_types, scopes, redirect_uri, nonce, state,
			force_approval_prompt, logged_in,
			claims_user_id, claims_username, claims_preferred_username,
			claims_email, claims_email_verified, claims_groups,
			connector_id, connector_data,
			expiry
		)
		values (
			$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18
		);
	`,
		a.ID, a.ClientID, encoder(a.ResponseTypes), encoder(a.Scopes), a.RedirectURI, a.Nonce, a.State,
		a.ForceApprovalPrompt, a.LoggedIn,
		a.Claims.UserID, a.Claims.Username, a.Claims.PreferredUsername,
		a.Claims.Email, a.Claims.EmailVerified, encoder(a.Claims.Groups),
		a.ConnectorID, a.ConnectorData,
		a.Expiry,
	)
	if err != nil {
		if c.alreadyExistsCheck(err) {
			return storage.ErrAlreadyExists
		}
		return fmt.Errorf("insert auth request: %v", err)
	}
	return nil
}

后端在认证成功后会返回IDtoken,并存储在dex-storage中,其中包含了用户的信息。

// Claims represents the ID Token claims supported by the server.
type Claims struct {
	UserID            string
	Username          string
	PreferredUsername string
	Email             string
	EmailVerified     bool

	Groups []string
}

三、 dex-server:

在dex服务端配置允许登录的dex客户端。staticClients段配置的是该dexserver允许接入的dexclient(第三方应用)信息,这个要跟dexclient那边的配置一致。在dex-server的配置文件中会设置dex存储链接方式和connector选项。

服务启动命令:

./bin/dex serve examples/config-dev.yaml
issuer: http://127.0.0.1:5556/dex
storage:
  type: sqlite3
  config:
    file: examples/dex.db
web:
  http: 0.0.0.0:5556

connectors:
- type: ldap
  name: OpenLDAP
  id: ldap
  config:
    host: localhost:10389

    # No TLS for this setup.
    insecureNoSSL: true

    # This would normally be a read-only user.
    bindDN: cn=admin,dc=example,dc=org
    bindPW: admin

    usernamePrompt: Email Address

    userSearch:
      baseDN: ou=People,dc=example,dc=org
      filter: "(objectClass=person)"
      username: mail
      # "DN" (case sensitive) is a special attribute name. It indicates that
      # this value should be taken from the entity's DN not an attribute on
      # the entity.
      idAttr: DN
      emailAttr: mail
      nameAttr: cn

    groupSearch:
      baseDN: ou=Groups,dc=example,dc=org
      filter: "(objectClass=groupOfNames)"

      userMatchers:
        # A user is a member of a group when their DN matches
        # the value of a "member" attribute on the group entity.
      - userAttr: DN
        groupAttr: member

      # The group name should be the "cn" value.
      nameAttr: cn

staticClients:
- id: example-app
  redirectURIs:
  - 'http://127.0.0.1:5555/callback'
  name: 'Example App'
  secret: ZXhhbXBsZS1hcHAtc2VjcmV0
  • dex-server认证流程:

    1.解析oauth2-client发来的http请求,解析为AuthRequest结构体

    2.把AuthRequest请求备份到dex-storage

    3.根据请求中的connector_id,找出dex-storage中的该connector的具体信息。检索dex-storage存储中的连接器对象。 该列表包括ConfigMap中定义的静态连接器和从存储中检索的动态连接器。

    4.根据connector的信息进行登录认证

// handleAuthorization handles the OAuth2 auth endpoint.
func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
	authReq, err := s.parseAuthorizationRequest(r)
	if err != nil {
		s.logger.Errorf("Failed to parse authorization request: %v", err)
		status := http.StatusInternalServerError

		// If this is an authErr, let's let it handle the error, or update the HTTP
		// status code
		if err, ok := err.(*authErr); ok {
			if handler, ok := err.Handle(); ok {
				// client_id and redirect_uri checked out and we can redirect back to
				// the client with the error.
				handler.ServeHTTP(w, r)
				return
			}
			status = err.Status()
		}

		s.renderError(r, w, status, err.Error())
		return
	}

	// TODO(ericchiang): Create this authorization request later in the login flow
	// so users don't hit "not found" database errors if they wait at the login
	// screen too long.
	//
	// See: https://github.com/dexidp/dex/issues/646
	authReq.Expiry = s.now().Add(s.authRequestsValidFor)
	if err := s.storage.CreateAuthRequest(*authReq); err != nil {
		s.logger.Errorf("Failed to create authorization request: %v", err)
		s.renderError(r, w, http.StatusInternalServerError, "Failed to connect to the database.")
		return
	}

	connectors, err := s.storage.ListConnectors()
	if err != nil {
		s.logger.Errorf("Failed to get list of connectors: %v", err)
		s.renderError(r, w, http.StatusInternalServerError, "Failed to retrieve connector list.")
		return
	}

	// Redirect if a client chooses a specific connector_id
	if authReq.ConnectorID != "" {
		for _, c := range connectors {
			if c.ID == authReq.ConnectorID {
				http.Redirect(w, r, s.absPath("/auth", c.ID)+"?req="+authReq.ID, http.StatusFound)
				return
			}
		}
		s.tokenErrHelper(w, errInvalidConnectorID, "Connector ID does not match a valid Connector", http.StatusNotFound)
		return
	}

	if len(connectors) == 1 && !s.alwaysShowLogin {
		for _, c := range connectors {
			// TODO(ericchiang): Make this pass on r.URL.RawQuery and let something latter
			// on create the auth request.
			http.Redirect(w, r, s.absPath("/auth", c.ID)+"?req="+authReq.ID, http.StatusFound)
			return
		}
	}

	connectorInfos := make([]connectorInfo, len(connectors))
	for index, conn := range connectors {
		connectorInfos[index] = connectorInfo{
			ID:   conn.ID,
			Name: conn.Name,
			Type: conn.Type,
			// TODO(ericchiang): Make this pass on r.URL.RawQuery and let something latter
			// on create the auth request.
			URL: s.absPath("/auth", conn.ID) + "?req=" + authReq.ID,
		}
	}

	if err := s.templates.login(r, w, connectorInfos, r.URL.Path); err != nil {
		s.logger.Errorf("Server template error: %v", err)
	}
}

四、 dex-client:

dex-client首先是根据一系列参数构造出oidc.Provideroidc.IDTokenVerifier,后面获取认证系统的跳转地址、获取id_token、校验id_token都会用到。第三方应用需要编写dex-client端的代码需要和dex-server进行交互,流程为

  1. 服务端配置dex-client的信息,只有该dex-client信息已经在dex-server中配置,相应的dex-client才能进行交互

    - id: example-app
      secret: example-app-secret
      name: 'Example App'
      # Where the app will be running.
      redirectURIs:
      - 'http://127.0.0.1:5555/callback'
    
  2. 用dex-server端配置的issuer URL,在第三方应用(client-app)中初始化OIDC身份验证服务。

// Initialize a provider by specifying dex's issuer URL.
provider, err := oidc.NewProvider(ctx, "https://dex-issuer-url.com")
if err != nil {
    // handle error
}
  1. 配置OAuth2的客户端配置
// Configure the OAuth2 config with the client values.
oauth2Config := oauth2.Config{
    // client_id and client_secret of the client.
    ClientID:     "example-app",
    ClientSecret: "example-app-secret",

    // The redirectURL.
    RedirectURL: "http://127.0.0.1:5555/callback",

    // Discovery returns the OAuth2 endpoints.
    Endpoint: provider.Endpoint(),

    // "openid" is a required scope for OpenID Connect flows.
    //
    // Other scopes, such as "groups" can be requested.
    Scopes: []string{oidc.ScopeOpenID, "profile", "email", "groups"},
}
  1. 创建ID token的解析器
// Create an ID token parser.
idTokenVerifier := provider.Verifier(&oidc.Config{ClientID: "example-app"})
  1. 然后,客户端发送http请求,请求中包含(client_id\CA\connector_id\等)HTTP服务器应将未经身份验证的用户重定向到dex,dex服务端验证dex-client, 后开始处理用户登录认证。
```
The HTTP server should then redirect unauthenticated users to dex to initialize the OAuth2 flow.

​```
// handleRedirect is used to start an OAuth2 flow with the dex server.
func handleRedirect(w http.ResponseWriter, r *http.Request) {
    state := newState()
    http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusFound)
}
  1. dex验证用户的身份后,它将使用可以交换ID令牌的代码将用户重定向回客户端应用程序。 然后,可以通过上面创建的验证程序来解析ID令牌。
func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) {
    state := r.URL.Query().Get("state")

    // Verify state.

    oauth2Token, err := oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
    if err != nil {
        // handle error
    }

    // Extract the ID Token from OAuth2 token.
    rawIDToken, ok := oauth2Token.Extra("id_token").(string)
    if !ok {
        // handle missing token
    }

    // Parse and verify ID Token payload.
    idToken, err := idTokenVerifier.Verify(ctx, rawIDToken)
    if err != nil {
        // handle error
    }

    // Extract custom claims.
    var claims struct {
        Email    string   `json:"email"`
        Verified bool     `json:"email_verified"`
        Groups   []string `json:"groups"`
    }
    if err := idToken.Claims(&claims); err != nil {
        // handle error
    }
}

五、 connector: dex-server端connector的信息,存储在static静态文件和动态的dex-storage中。决定了dex-server对接的OIDC的认证方案的配置。

你可能感兴趣的:(认证授权)