IdP dex에 대해 알아보자!

DEX란?

  • 여러 IdP와 호환되는 플러그인들을 제공하여 일종의 connector와 같이 동작
  • client app은 dex만 바라보고 인증로직을 짜면되고, 뒷단의 IdP는 dex에서 관리
  • 제공하는 플러그인은 github, LDAP, SAML, 등등

Document : https://dexidp.io/docs/
OIDC에 대한 설명 : 호롤리/호다닥 공부해보는 SSO와 친구들 (SAML, OAuth, OIDC)

Tutorial

1. dex 빌드 및 실행

(21.11.17) go 버전은 1.15이상이어야 합니다.
dex github -> https://github.com/dexidp/dex

$ git clone https://github.com/dexidp/dex.git
$ cd dex/
$ make build

dex는 실행시킬 때 config파일을 파라미터로 받아서 실행됩니다.
클론받은 dex/example폴더에는 여러 예제들이 있고 지금은 example/config-dev.yaml파일을 가지고 실행시켜보겠습니다.

$ pwd
/home/user/dex

$ ./bin/dex  serve examples/ldap/config-ad.yaml

실행시키면 dex서버는 5556번 포트를 사용하여 올라오게 됩니다.

2. sample client app 실행

example app 빌드

$ pwd
/home/user/dex

$ make example

실행

$ ./bin/example-app
2022/11/07 06:18:15 listening on http://127.0.0.1:5555

3. 로그인 테스트

Login버튼을 누르면 dex로 redirect됩니다.

Log in with Email 선택

초기 계정 -> admin@example.com/password

Client app에서 요청하는 권한이 무엇인지 보여주는 페이지

허가하면 dex에서 받은 토큰값들을 Client 페이지에서 확인 가능

Config파일 살펴보기

샘플을 한번 돌려보았으니, 위에서 사용했던 sample config파일을 살펴보도록 하겠습니다.

Github -> https://github.com/dexidp/dex/blob/master/examples/config-dev.yaml

# 외부(Client)에서 접근 가능한 dex위치
issuer: http://127.0.0.1:5556/dex

# dex 상태저장용 db configuration
storage:
  type: sqlite3
  config:
    file: examples/dex.db

# dex HTTP endpoint
web:
  http: 0.0.0.0:5556

# Configuration for telemetry
telemetry:
  http: 0.0.0.0:5558

# Client 정보, clientId, redirect uri, secret 등
staticClients:
- id: example-app
  redirectURIs:
  - 'http://127.0.0.1:5555/callback'
  name: 'Example App'
  secret: ZXhhbXBsZS1hcHAtc2VjcmV0

# IdP 정보, 현재는 sample이라 mock처리됨
connectors:
- type: mockCallback
  id: mock
  name: Example

# Let dex keep a list of passwords which can be used to login to dex.
enablePasswordDB: true

# A static list of passwords to login the end user. By identifying here, dex
# won't look in its underlying storage for passwords.
#
# If this option isn't chosen users may be added through the gRPC API.
staticPasswords:
- email: "admin@example.com"
  # bcrypt hash of the string "password": $(echo password | htpasswd -BinC 10 admin | cut -d: -f2)
  hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
  username: "admin"
  userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"

Ex1) Active Directory와 연결하기

✔ Active Directory는 이미 구축된 것을 전제
✔ Active Directory는 LDAP기반이라 LDAP용 config를 그대로 사용합니다.
LDAP용 sample config -> https://github.com/dexidp/dex/blob/master/examples/ldap/config-ldap.yaml

AD에서의 유저 정보를 파악하기 위해 ldapsearch 커맨드를 사용합니다.

$ ldapsearch -h {LDAP_SERVER} -b "CN=test,CN=Users,DC=adfs,DC=local" -D "Administrator@adfs.local" -w "76PPIWZBHpoQL339tH9O"

# extended LDIF
#
# LDAPv3
# base <CN=test,CN=Users,DC=adfs,DC=local> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# test, Users, adfs.local
dn: CN=test,CN=Users,DC=adfs,DC=local
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: user
cn: test
givenName: test
distinguishedName: CN=test,CN=Users,DC=adfs,DC=local
instanceType: 4
whenCreated: 20221109161554.0Z
whenChanged: 20221110155359.0Z
displayName: test
uSNCreated: 12816
memberOf: CN=testGroup,CN=Users,DC=adfs,DC=local
uSNChanged: 13827
name: test
objectGUID:: W8IMeqB/akqJR4X8reHI8Q==
userAccountControl: 512
badPwdCount: 0
codePage: 0
countryCode: 0
badPasswordTime: 133125689091224229
lastLogoff: 0
lastLogon: 133125692391848639
pwdLastSet: 133124841542578900
primaryGroupID: 513
objectSid:: AQUAAAAAAAUVAAAAxjuNl/RArVbl+IgvUAQAAA==
accountExpires: 9223372036854775807
logonCount: 0
sAMAccountName: test
sAMAccountType: 805306368
userPrincipalName: test@adfs.local
objectCategory: CN=Person,CN=Schema,CN=Configuration,DC=adfs,DC=local
dSCorePropagationData: 16010101000000.0Z
lastLogonTimestamp: 133125692391848639

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

이제 요 정보를 기반으로 config를 수정해줄겁니다.

dex config파일 :

issuer: http://127.0.0.1:5556/dex
storage:
  type: sqlite3
  config:
    file: examples/dex.db
web:
  http: 0.0.0.0:5556

# AD configuration
connectors:
- type: ldap
  name: OpenLDAP
  id: ldap
  config:
    # 1) Plain LDAP, without TLS:
    # 기본적으로 AD의 port는 389
    host: xx.xx.xx.xx:389
    insecureNoSSL: true
    #
    # 2) LDAPS without certificate validation:
    #host: localhost:636
    #insecureNoSSL: false
    #insecureSkipVerify: true
    #
    # 3) LDAPS with certificate validation:
    #host: YOUR-HOSTNAME:636
    #insecureNoSSL: false
    #insecureSkipVerify: false
    #rootCAData: 'CERT'
    # ...where CERT="$( base64 -w 0 your-cert.crt )"

    # LDAP search를 진행할 인증용 계정
    bindDN: cn=Administrator,cn=Users,dc=adfs,dc=local
    bindPW: 76PPIWZBHpoQL339tH9O

    # Login 화면에서 보여줄 label
    usernamePrompt: AD Username

    userSearch:
      # User search의 baseDN
      baseDN: cn=Users,dc=adfs,dc=local

      # 아래 두개는 검색 필터 
      filter: "(objectClass=user)"
      username: name #({username}=입력한ID)

      # user정보와 매핑되는 attribute들
      idAttr: sAMAccountType
      emailAttr: userPrincipalName
      nameAttr: sAMAccountName

    groupSearch:
      # BaseDN to start the search from. It will translate to the query
      # "(&(objectClass=group)(member=<user uid>))".
      baseDN: dc=adfs,dc=local
      filter: "(objectClass=group)"
      userMatchers:
        # A user is a member of a group when their DN matches
        # the value of a "member" attribute on the group entity.
      - userAttr: distinguishedName
        groupAttr: member
      # The group name should be the "cn" value.
      nameAttr: cn

# Client 정보
staticClients:
- id: example-app
  redirectURIs:
  - 'http://127.0.0.1:5555/callback'
  name: 'Example App'
  secret: ZXhhbXBsZS1hcHAtc2VjcmV0

이 config로 dex를 띄우고나서 로그인을 시도하면 아래와 같은 로그를 확인할 수 있습니다.

time="2022-11-10T11:10:39Z" level=info msg="performing ldap search cn=Users,dc=adfs,dc=local sub (&(objectClass=user)(name=test))"
time="2022-11-10T11:10:39Z" level=info msg="username \"test\" mapped to entry CN=test,CN=Users,DC=adfs,DC=local"
time="2022-11-10T11:10:39Z" level=info msg="login successful: connector \"ldap\", username=\"test\", preferred_username=\"\", email=\"test@adfs.local\", groups=[]"

Client App에서 필요한 유저정보가 다를 수 있고 search 기준이 다를 수 있기 때문에 거기에 맞춰서 userSearch항목과 groupSearch항목을 수정하면 되겠습니다.

Ex2) ADFS OIDC와 연결하기

✔ ADFS는 이미 구축된 것을 전제 (참고 -> ADFS OIDC 구성하기)
OIDC용 sample config -> https://dexidp.io/docs/connectors/oidc/

connectors:
- type: oidc
  id: adfs
  name: ADFS
  config:
    # /.well-known/openid-configuration에서 확인할 수 있는 issuer의 url
    issuer: https://adfs.xxx.com/adfs

    clientID: 1b7d1b49-8c67-abcd-abcd-94eeabcdae78
    #clientSecret:

    # Dex's issuer URL + "/callback"
    redirectURI: https://dex.xxx.com:5556/dex/callback

    # List of additional scopes to request in token response
    # Default is profile and email
    # Full list at https://dexidp.io/docs/custom-scopes-claims-clients/
    scopes:
     - profile
     - email
     - groups

    # Some providers return claims without "email_verified", when they had no usage of emails verification in enrollment process
    # or if they are acting as a proxy for another IDP etc AWS Cognito with an upstream SAML IDP
    # This can be overridden with the below option
    insecureSkipEmailVerified: true

    # Groups claims (like the rest of oidc claims through dex) only refresh when the id token is refreshed
    # meaning the regular refresh flow doesn't update the groups claim. As such by default the oidc connector
    # doesn't allow groups claims. If you are okay with having potentially stale group claims you can use
    # this option to enable groups claims through the oidc connector on a per-connector basis.
    # This can be overridden with the below option
    insecureEnableGroups: true

    # When enabled, the OpenID Connector will query the UserInfo endpoint for additional claims. UserInfo claims
    # take priority over claims returned by the IDToken. This option should be used when the IDToken doesn't contain
    # all the claims requested.
    # https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
    #getUserInfo: true

    # The set claim is used as user id.
    # Claims list at https://openid.net/specs/openid-connect-core-1_0.html#Claims
    # Default: sub
    # userIDKey: nickname

    # The set claim is used as user name.
    Default: name
    userNameKey: upn

    claimMapping:
      # The set claim is used as preferred username.
      Default: preferred_username
      preferred_username: unique_name

      # The set claim is used as email.
      Default: email
      email: upn

      # The set claim is used as groups.
      Default: groups
      groups: role

ClaimMapping에 관해서는 Custom Claim 추가하기문서를 참조

Ex3) ADFS SAML과 연결하기

✔ ADFS는 이미 구축된 것을 전제 (참고 -> ADFS SAML 2.0 구성하기)
✔ SAML용 sample config -> https://dexidp.io/docs/connectors/saml/
⚠ 현재 DEX의 SAML connector는 더이상 maintain되지 않고 있습니다. 참고 -> Proposal: deprecate the SAML connector
⚠ SAML encrypted Assertion을 지원하지 않음!!!!

- type: saml
  id: dex
  name: SAML2.0
  config:
    # SAML endpoint (ADFS Management의 Service>endpoint 참조)
    ssoURL: https://adfs.xxx.com/adfs/ls/idpinitiatedsignon

    # SAML Response를 validation할때 사용할 certification 명시 or Insecure
    insecureSkipSignatureValidation: true
    #ca: /home/vpcuser/dex/cert.pem

    redirectURI: https://dex.com:5556/dex/callback
    entityIssuer: https://dex.com:5556/dex/callback

    # SAML assertion의 어떤 attribute들과 매핑할건지
    usernameAttr: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
    emailAttr: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress

    # NameID가 user의 unique id로 매핑되는데 어떤 형식으로 요청할 건지 설정
    # default는 persistent (ex. DOMAIN\USERNAME)
    # emailAddress (ex. USERNAME@xxx.com)
    nameIDPolicyFormat: emailAddress

Troubleshooting

nameIdPolicyFormat

nameIDPolicyFormat 문제에 관해서는 다음 문서 참조 -> Error : The SAML request contained a NameIDPolicy that was not satisfied by the issued token

DEX의 SAML connector는 encrypted된 assertion을 지원하지 않습니다.
그래서 만약 encrypted된 assertion을 받게 된다면 아래와 같이 assertion이 없다고 뜨게 됩니다.

해결하려면 다음 문서 참조 -> Error : The requested relying party trust ‘’ is unspecified or unsupported. 혹은 response did not contain an assertionPermalink

PANIC: nil pointer dereference exception

2022/12/13 11:55:40 http2: panic serving xx.xx.xx.xx:63438: runtime error: invalid memory address or nil pointer dereference
goroutine 93 [running]:
net/http.(*http2serverConn).runHandler.func1()
        /usr/local/go/src/net/http/h2_bundle.go:5904 +0x125
panic({0x1361b80, 0x2197930})

뜬금없이 nil pointer에러가 발생할수도 있습니다…

정확한 원인은 찾지 못했고 이미 dex 레포에 이슈도 올라가있는 상태이지만, “Proposal: deprecate the SAML connector“문제로 더이상 SAML connector는 maintain되지 않기 때문에 언제 해결될지도 모르겠네요.

근데 추측컨대, dex에서 encrypted assertion을 제대로 decrypt하지 못해서 발생한 문제같습니다.
사용했던 CA가 잘못되었을수도 있구요…

일단 제가 썼던 workaround를 남겨둡니다.

  1. ADFS SAML configuration에서 encrypted assertion기능을 끔 (참고: encrypted assertion끄기)
  2. DEX configuration파일에서 ca항목을 주석처리
  3. insecureSkipSignatureValidation: true로 세팅

Appendix

IdP Metadata 얻기

dex는 OIDC기반이므로 다른 OIDC서비스들과 마찬가지로 metadata를 .well-known/openid-configuration path로 얻을 수 있습니다.

$ curl x.x.x.x:5556/dex/.well-known/openid-configuration

{
  "issuer": "http://x.x.x.x:5556/dex",
  "authorization_endpoint": "http://x.x.x.x:5556/dex/auth",
  "token_endpoint": "http://x.x.x.x:5556/dex/token",
  "jwks_uri": "http://x.x.x.x:5556/dex/keys",
  "userinfo_endpoint": "http://x.x.x.x:5556/dex/userinfo",
  "device_authorization_endpoint": "http://x.x.x.x:5556/dex/device/code",
  "grant_types_supported": [
    "authorization_code",
    "refresh_token",
    "urn:ietf:params:oauth:grant-type:device_code"
  ],
  "response_types_supported": [
    "code"
  ],
  "subject_types_supported": [
    "public"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "code_challenge_methods_supported": [
    "S256",
    "plain"
  ],
  "scopes_supported": [
    "openid",
    "email",
    "groups",
    "profile",
    "offline_access"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic",
    "client_secret_post"
  ],
  "claims_supported": [
    "iss",
    "sub",
    "aud",
    "iat",
    "exp",
    "email",
    "email_verified",
    "locale",
    "name",
    "preferred_username",
    "at_hash"
  ]
}

댓글남기기