owncloud / ocis

:atom_symbol: ownCloud Infinite Scale Stack
https://doc.owncloud.com/ocis/next/
Apache License 2.0
1.4k stars 183 forks source link

Login problem with Janssen OIDC+LDAP #8506

Closed tailnet-h-usky-io closed 8 months ago

tailnet-h-usky-io commented 8 months ago

Describe the bug

Hello, OCIS users.

I am attempting to integrate OwnCloud Infinite Scale (v4.0.x) with the [Janssen open-source IDP][1], and have gotten as far as completing the OpenID Connect token authorization + user info response. But then, OwnCloud tries to post some user data to itself and gets a 401, so login fails and I wind up at the path /access-denied:

image

Here are the specifics of the requests and responses:

  1. User visits OwnCloud and is redirected to Janssen IDP for login
  2. Upon successful authentication to Janssen, user is presented with the OIDC scopes (username, email, and profile, I think?) for approval, as per usual. So far, so good...
  3. Upon user approval of OIDC scopes, browser is redirected to OwnCloud's OIDC callback page (oidc-callback), which then requests the Access Token. This succeeds... image
  4. The OwnCloud callback page requests the User info from the IDP. This also succeeds: image I've noticed that when I login with the Janssen default admin account, more OIDC claims are returned than for this test user. Not sure why yet, or whether that's related, but if anyone knows what OCIS requires in order to login from this IDP response, I'd love to know...
  5. The OwnCloud callback page makes a POST request to itself, specifically to /api/v0/settings/values-list with the following payload: {"account_uuid": "me"} (huh?) . This request is denied (or blocked? - the browser shoes no actual bytes in the Response...) but I'm not sure why. image
  6. A subsequent request to /ocs/v1.php/cloud/user with type application/x-www-form-urlencoded is similarly blocked/denied.

Initial clues

The main clues which I think I have so far are that:

  1. the Response Headers in the screenshot above shows the Server value of rpxy; and
  2. the OwnCloud logs do not report any login error, even in debug mode. (Though the frontend service does log the token exchange)

This makes me suspect that the POST to values-list might be blocked by the [rpxy proxy server][3] which I have set up in front of OwnCloud, and might not be reaching OCIS at all.

Why the extra proxy, when OCIS already has a proxy Service? There are two reasons: to terminate TLS; and to acquire an overlay IP address.

You see, what makes this setup a little unusual is that all the software services are running on an overlay ([Tailscale][2]) network, so nothing is reachable from the internet. Both OwnCloud and Janssen are deployed with Helm onto a microk8s setup, and Tailscale provides a custom [Kubernetes Operator][4] that attaches an overlay address to any existing K8s Service resource. The Tailscale K8s Operator can also create an Ingress resource with an overlay IP, but in that case the Operator uses the Tailscale-assigned DNS name and generates a TLS certificate on-the-fly. I prefer to use my own internal, private sub-domain with my own DNS servers and a purchased wildcard cert.

So, the inbound traffic for OwnCloud first arrives at the rpxy proxy Service's Tailscale-assigned IP, where it's forwarded to the rpxy container. rpxy then terminates HTTPs and forwards the request to the K8s-assigned Service IP for OwnCloud's proxy Service (via its CoreDNS-assigned DNS name). Here's the rpxy config:

listen_port = 80
listen_port_tls = 443

default_application = "ocis"

[apps."ocis"]
server_name = 'oc.h.usky.io'
tls = { https_redirection = true, tls_cert_path = '/h.usky.io.crt', tls_cert_key_path = '/h.usky.io.key' }

# 
reverse_proxy = [
    { upstream = [
        { location = 'proxy.ocis.svc.cluster.local:9200' },
    ] },
]

^^ note that proxy.ocis.svc.cluster.local is the name CoreDNS assigns to OwnCloud's proxy service when deployed via Helm charts.

This traffic flow seems to work fine, until the request from the OIDC redirect page to /api/v0/settings/values-list - at which point it fails, perhaps blocked by rpxy (?). This is puzzling, since the blocked requests are not cross-domain - they're from https://oc.h.usky.io to https://oc.h.usky.io, so I don't see that CORS rules should apply here.

Steps to reproduce

  1. Deploy OCIS and Janssen with Helm charts. Use the following overrides for OCIS:
    
    http:
    cors:
    allow_origins: ["https://sso.[some domain]", "https://oc.[some domain]", "https://[some domain]"]

externalDomain: oc.[some domain]

features: basicAuthentication: false demoUsers: false

externalUserManagement: enabled: true adminUUID: "xxxxxxxxxxxxxxxxxxxxx" oidc: issuerURI: https://sso.[some domain] webClientID: [OIDC client ID] userIDClaim: inum userIDClaimAttributeMapping: userid

  accessTokenVerifyMethod: "jwt"

  roleAssignment:
    enabled: true
    claim: User Permission
    mapping:
      - role_name: admin
        claim_value: ocisAdmin
      - role_name: spaceadmin
        claim_value: ocisSpaceAdmin
      - role_name: user
        claim_value: ocisUser
      - role_name: guest
        claim_value: ocisGuest

ldap:
  writeable: true
  readOnlyAttributes:
  uri: ldaps://ldap.[some domain]:1636
  bindDN: cn=Directory Manager
  refintEnabled: false
  passwordModifyExOpEnabled: false
  useServerUUID: true
  user:
    schema:
      id: inumattribute of Active Directory for the user ID`s.
      idIsOctetString: false
      mail: Email
      displayName: displayName
      userName: User Name
      userType: employeeType
    baseDN: ou=people,o=jans
    scope: subdoing suffix only searches or `any` for doing full substring searches
    substringFilterType: any
    objectClass: jansPerson
  group:
    schema:
      id: inumattribute of Active Directory for the group ID`s.
      idIsOctetString: false
      mail: mail
      displayName: displayName
      groupName: inum
      member: member
    baseDN: ou=groups,o=jans
    scope: sub
    filter:
    objectClass: jansGrp
  disableUsers:
    disableMechanism: attribute
    userEnabledAttribute: jansActive

2. Deploy `rpxy` in front of OCIS
3. Attempt login

## Expected behavior
Completed login request flow.

## Actual behavior
_Nearly completed_  request flow as described above.

## Setup
See above.

I understand these conditions are not easy to reproduce, and don't expect anyone here to try. I'm just looking for debugging suggestions or other ideas!

Thanks in advance,
-b

[1]:https://github.com/JanssenProject/jans
[2]:https://tailscale.com
[3]:https://github.com/junkurihara/rust-rpxy
[4]:https://tailscale.com/kb/1236/kubernetes-operator
rhafer commented 8 months ago

I understand these conditions are not easy to reproduce, and don't expect anyone here to try. I'm just looking for debugging suggestions or other ideas!

You probably want to look at the logs for the ocis proxy service first (ideally with log level "debug"). If you don't see the /api/v0/settings/values-list request anywhere it that log, there is likely something wrong with you reserve proxy's setup (I fear I can't really help you with debugging that). If you see anything about that values-list request in the ocis proxy service log, it should also give you hints about why it fails to authenticate the request. If possible, please attach the log here.

tailnet-h-usky-io commented 8 months ago

Thanks for your suggestion, @rhafer! I found the log entries for two the denied requests in the logs for OwnCloud's proxy service, just as you suggested:

Proxy log

It says failed to verify access token: token contains an invalid number of segments

2024-02-23T02:53:30Z DBG director found line=github.com/owncloud/ocis/v2/services/proxy/pkg/router/router.go:222 method=POST path=/api/v0/settings/values-list policy=ocis prefix=/api/v0/settings routeType=prefix service=proxy
2024-02-23T02:53:30Z ERR failed to authenticate the request error="failed to verify access token: token contains an invalid number of segments" authenticator=oidc line=github.com/owncloud/ocis/v2/services/proxy/pkg/middleware/oidc_auth.go:165 path=/api/v0/settings/values-list service=proxy
2024-02-23T02:53:30Z INF access-log bytes=0 duration=0.262592 line=github.com/owncloud/ocis/v2/services/proxy/pkg/middleware/accesslog.go:31 method=POST path=/api/v0/settings/values-list proto=HTTP/1.1 remote-addr=10.1.24.21 request-id=0e4f18d3-d270-4d12-85b1-d35cd5a31f90 service=proxy status=401
2024-02-23T02:53:30Z DBG director found line=github.com/owncloud/ocis/v2/services/proxy/pkg/router/router.go:222 method=GET path=/ocs/v1.php/cloud/user policy=ocis prefix=/ocs/ routeType=prefix service=proxy
2024-02-23T02:53:30Z ERR failed to authenticate the request error="failed to verify access token: token contains an invalid number of segments" authenticator=oidc line=github.com/owncloud/ocis/v2/services/proxy/pkg/middleware/oidc_auth.go:165 path=/ocs/v1.php/cloud/user service=proxy
2024-02-23T02:53:30Z INF access-log bytes=0 duration=0.164137 line=github.com/owncloud/ocis/v2/services/proxy/pkg/middleware/accesslog.go:31 method=GET path=/ocs/v1.php/cloud/user proto=HTTP/1.1 remote-addr=10.1.24.21 request-id=53d571c0-dbe5-4dac-a9da-c6f9709803af service=proxy status=401

Browser Debug Console

image

If I right-click on the failed request in the JS Console and select Copy object (Chrome/Macos), I get:

{
    "message": "Request failed with status code 401",
    "name": "AxiosError",
    "stack": "AxiosError: Request failed with status code 401\n    at pSe (https://oc.h.usky.io/js/chunks/vendor-163bae29.mjs:16:966)\n    at XMLHttpRequest.f (https://oc.h.usky.io/js/chunks/vendor-163bae29.mjs:16:4125)",
    "config": {
        "transitional": {
            "silentJSONParsing": true,
            "forcedJSONParsing": true,
            "clarifyTimeoutError": false
        },
        "adapter": [
            "xhr",
            "http"
        ],
        "transformRequest": [
            null
        ],
        "transformResponse": [
            null
        ],
        "timeout": 0,
        "xsrfCookieName": "XSRF-TOKEN",
        "xsrfHeaderName": "X-XSRF-TOKEN",
        "maxContentLength": -1,
        "maxBodyLength": -1,
        "env": {},
        "headers": {
            "Accept": "application/json, text/plain, */*",
            "Content-Type": "application/json",
            "Accept-Language": "en",
            "Authorization": "Bearer 6afb5667-2a96-448c-a9e2-5ac6c40e538f",
            "X-Requested-With": "XMLHttpRequest",
            "X-Request-ID": "83f0f03b-9d40-423f-8f87-d0019359f409"
        },
        "baseURL": "https://oc.h.usky.io/",
        "cancelToken": {
            "promise": {},
            "_listeners": []
        },
        "method": "post",
        "url": "/api/v0/settings/values-list",
        "data": "{\"account_uuid\":\"me\"}"
    },
    "code": "ERR_BAD_REQUEST",
    "status": 401
}

Questions

Three questions arise immediately...

  1. The code at middleware/oidc_auth.go:165 (from the proxy log) looks like it's trying to decode the OIDC claims from the auth token using the function getClaims(token, r) – is that right?

  2. When the proxy service says failed to verify access token: token contains an invalid number of segments, is it referring to this value: Bearer 6afb5667-2a96-448c-a9e2-5ac6c40e538f ? Is there a way to check this value in some kind of token decoder?

  3. Is the expected return value of the failing function getClaims(token, r) somehow related to the OIDC claims that are already being returned from the (successful) request made immediately before the failing request (to the IDP's user-info endpoint)? Do they contain similar / overlapping info? For reference, here's the corresponding response body from user-info:

{
    "sub": "d48378e5-4f53-406d-afe9-9d58234f66f2",
    "updated_at": 1708658900,
    "user_name": "test",
    "jans_status": "active",
    "name": "Test User",
    "nickname": "testy",
    "user_permission": [
        "ocisUser"
    ],
    "given_name": "Test",
    "family_name": "User",
    "email": "testuser@h.usky.io"
}

I tried to tweak the Janssen IDP server to match this JSON to the OwnCLoud's OIDC config:

    oidc:
      issuerURI: https://sso.h.usky.io
      webClientID: [xxxxxxx]...
      userIDClaim: sub
      userIDClaimAttributeMapping: userid
      accessTokenVerifyMethod: "jwt"
      roleAssignment:
        enabled: true
        claim: user_permission
        mapping:
          - role_name: admin
            claim_value: ocisAdmin
          - role_name: spaceadmin
            claim_value: ocisSpaceAdmin
          - role_name: user
            claim_value: ocisUser
          - role_name: guest
            claim_value: ocisGuest

... so, I've tried to set the config so this test user has the claim ocisUser, which should map to the OCIS role user. Does that look correct?

Next Steps

Um..... try to decode the token value externally? Update to OCIS version 5 (looks like my container images are v4.0.1)?

Again, thanks for your suggestion!

tailnet-h-usky-io commented 8 months ago

OK, after trying both my Next Steps, I determined that the Janssen OIDC provider server was sending something called a "Reference Token", not "value token" / JWT. Here's the relevant client config setting:

image

^^ set this to JWT for OwnCloud!

For future reference, here is the basic client setup in Janssen for OCIS:

image

^^ I added the permission scope so that the user_permission OIDC claim can be used to map the default Janssen admin user into the OCIS admin Role, but that's not really the correct way to apply custom claims when using Janssen LDAP, so I'll probably fiddle with this some more to setup my other users.

Anyway, once I fixed the token format and nailed down the LDAP settings, I can log in with the admin user created by the Janssen Helm charts. Closing this issue.