Microsoft ADFS — Admin UI & S3 Setup

Setup guide for using Microsoft Active Directory Federation Services (ADFS) with SeaweedFS Enterprise: signing operators in to the Admin UI through ADFS as an OpenID Connect provider, and federating S3 API access with ADFS-issued tokens through STS — over OIDC or SAML 2.0.

ADFS on Windows Server 2016 or later works as a standard OpenID Connect provider, and OIDC is the recommended path for both the Admin UI and the S3 API. SAML 2.0 is also supported for the S3 STS path, for environments that federate through classic relying-party trusts.

What you want Protocol Configured in
Admin UI single sign-on OIDC security.toml[admin.oidc]
S3 API access with ADFS JWTs OIDC + STS AssumeRoleWithWebIdentity weed s3 -iam.config=<file>
S3 API access with SAML assertions SAML 2.0 + STS AssumeRoleWithSAML weed s3 -iam.config=<file>

Prerequisites

  • ADFS 2016+ (farm behavior level 2016 or later) for the OIDC paths. The OIDC discovery document must be reachable from the SeaweedFS servers:

    https://adfs.example.com/adfs/.well-known/openid-configuration
    
  • ADFS always serves HTTPS. If the ADFS service certificate is issued by a private CA (typical with AD CS), the SeaweedFS side needs the CA bundle — set tls_ca_cert in [admin.oidc], or tlsCaCert in the iam.config provider entry.

  • Active Directory groups you can map to roles, e.g. SeaweedFS-Admins and SeaweedFS-Operators.

Admin UI via ADFS (OIDC)

The admin server runs the standard authorization-code flow against ADFS. The general mechanics — role model, redirect-URL rules, startup behavior — are in the Admin UI OIDC Login reference; this section covers the ADFS-specific parts.

ADFS side

  1. In the ADFS Management console, add an Application Group using the Server application accessing a web API template.

  2. In the Server application step, note the generated Client Identifier, add the redirect URI, and generate a shared secret:

    https://admin.example.com:23646/login/oidc/callback
    

    Use the exact external URL your operators reach the admin UI on — behind a reverse proxy or -urlPrefix=/seaweedfs the path becomes https://admin.example.com/seaweedfs/login/oidc/callback.

  3. In the Web API step, set the identifier to the same client ID, and under Client permissions grant at least the openid and allatclaims scopes. allatclaims makes ADFS copy the issuance-transform claims (UPN, groups) into the ID token, which is what SeaweedFS validates.

  4. On the Web API, add Issuance Transform Rules:

    • Send LDAP Attributes as Claims: User-Principal-Name → outgoing claim type UPN.
    • Send LDAP Attributes as Claims: Token-Groups - Unqualified Names → outgoing claim type Group.

    In the resulting JWT, the UPN arrives as the short claim name upn; the group claim keeps its full claim-type URI http://schemas.xmlsoap.org/claims/Group as the claim name.

SeaweedFS side

Configure [admin.oidc] in security.toml:

[admin.oidc]
enabled = true
issuer = "https://adfs.example.com/adfs"
client_id = "3f09cdbe-63c9-4a03-a2b1-6ea4a5c3e2df"   # ADFS Client Identifier
client_secret = "replace-me"                          # ADFS shared secret
redirect_url = "https://admin.example.com:23646/login/oidc/callback"
scopes = ["openid", "allatclaims"]
username_claim = "upn"
groups_claim = "http://schemas.xmlsoap.org/claims/Group"
admin_groups = ["SeaweedFS-Admins"]
readonly_groups = ["SeaweedFS-Operators"]
tls_ca_cert = "/etc/seaweedfs/adfs-ca.pem"   # only if ADFS uses a private CA

The admin_groups / readonly_groups shortcuts match values of the claim named by groups_claim. For finer control, write explicit rules instead — each rule names its claim directly:

[admin.oidc.role_mapping]
default_role = "readonly"

[[admin.oidc.role_mapping.rules]]
claim = "http://schemas.xmlsoap.org/claims/Group"
value = "SeaweedFS-Admins"
role = "admin"

Notes:

  • issuer is https://<adfs-host>/adfs — SeaweedFS appends /.well-known/openid-configuration for discovery. Discovery runs when the admin server starts, so restart it after ADFS endpoint changes. HTTPS is required.
  • username_claim picks which claim becomes the displayed username. Without it, SeaweedFS resolves preferred_usernameupnunique_nameemailnamesub; ADFS tokens usually carry upn, so the chain normally lands there — set it explicitly to be deterministic.
  • groups_claim names the claim the admin_groups/readonly_groups shortcuts match against (default groups). ADFS emits groups under the full URI shown above, so either point groups_claim at that URI, or add a custom ADFS claim rule that re-issues the groups under the literal outgoing claim type groups and keep the default. Explicit role_mapping.rules are unaffected — each rule names its claim directly.
  • Both keys can also be set via environment: WEED_ADMIN_OIDC_USERNAME_CLAIM and WEED_ADMIN_OIDC_GROUPS_CLAIM.

Restart the admin server; a Sign in with OIDC button appears on the login page, and local break-glass credentials keep working alongside it.

S3 API via ADFS (OIDC + STS)

The S3 gateway can accept ADFS-issued JWTs directly. Configure an OIDC identity provider in the advanced IAM config and start the gateway with it:

weed s3 -iam.config=/etc/seaweedfs/iam.json
{
  "sts": {
    "tokenDuration": "1h",
    "issuer": "seaweedfs-sts",
    "signingKey": "<base64 random key>"
  },
  "providers": [
    {
      "name": "adfs",
      "type": "oidc",
      "enabled": true,
      "config": {
        "issuer": "https://adfs.example.com/adfs",
        "clientId": "3f09cdbe-63c9-4a03-a2b1-6ea4a5c3e2df",
        "jwksUri": "",
        "roleMapping": {
          "rules": [
            {
              "claim": "http://schemas.xmlsoap.org/claims/Group",
              "value": "SeaweedFS-Admins",
              "role": "arn:aws:iam::role/AdfsS3Admin"
            }
          ],
          "defaultRole": "arn:aws:iam::role/AdfsS3ReadOnly"
        }
      }
    }
  ],
  "roles": [
    {
      "roleName": "AdfsS3Admin",
      "roleArn": "arn:aws:iam::role/AdfsS3Admin",
      "trustPolicy": {
        "Version": "2012-10-17",
        "Statement": [
          {
            "Effect": "Allow",
            "Principal": { "Federated": "adfs" },
            "Action": ["sts:AssumeRoleWithWebIdentity"]
          }
        ]
      },
      "attachedPolicies": ["S3AdminPolicy"]
    }
  ],
  "policies": [
    {
      "name": "S3AdminPolicy",
      "document": {
        "Version": "2012-10-17",
        "Statement": [
          {
            "Effect": "Allow",
            "Action": ["s3:*", "sts:ValidateSession"],
            "Resource": ["*", "arn:aws:s3:::*", "arn:aws:s3:::*/*"]
          }
        ]
      }
    }
  ]
}

jwksUri is optional — with ADFS 2016+ it is filled in from OIDC discovery. Define one role and trust policy per level of access you want to hand out, and map ADFS groups to role ARNs in roleMapping.

Clients then exchange an ADFS JWT for temporary S3 credentials with AssumeRoleWithWebIdentity against the S3 endpoint:

# 1. Obtain a JWT from ADFS (any OAuth flow works; ROPC shown for scripting).
#    The password is read from stdin so it never appears in process arguments
#    or shell history.
read -rs -p "ADFS password: " ADFS_PASSWORD; echo
JWT=$(printf %s "$ADFS_PASSWORD" | curl -s https://adfs.example.com/adfs/oauth2/token \
  -d grant_type=password -d client_id=3f09cdbe-63c9-4a03-a2b1-6ea4a5c3e2df \
  -d username=[email protected] -d scope=openid \
  --data-urlencode password@- \
  | jq -r .access_token)
unset ADFS_PASSWORD

# 2. Exchange it for temporary S3 credentials
aws sts assume-role-with-web-identity \
  --endpoint-url https://s3.example.com \
  --role-arn arn:aws:iam::role/AdfsS3Admin \
  --role-session-name alice \
  --web-identity-token "$JWT"

The response contains an access key, secret key, and session token usable with any S3 client until expiry. Alternatively, clients can skip the STS exchange and send the ADFS JWT on each request as a Bearer token (Authorization: Bearer <JWT>); the gateway validates it against the provider and applies the mapped role’s policies.

S3 API via ADFS (SAML 2.0 + STS)

For environments that federate through SAML rather than OIDC, the S3 gateway supports SAML 2.0 identity providers and the STS AssumeRoleWithSAML action.

ADFS side

  1. Add a claims-aware Relying Party Trust whose identifier matches the spEntityId you configure in SeaweedFS (below).

  2. Add Issuance Transform Rules: a Name ID claim for the user identity, and Token-Groups - Unqualified Names → outgoing claim type Group.

  3. The ADFS federation metadata SeaweedFS consumes is published at:

    https://adfs.example.com/FederationMetadata/2007-06/FederationMetadata.xml
    

SeaweedFS side

Add a saml provider entry to the same iam.config file:

{
  "type": "saml",
  "name": "adfs",
  "enabled": true,
  "config": {
    "idpMetadataUrl": "https://adfs.example.com/FederationMetadata/2007-06/FederationMetadata.xml",
    "spEntityId": "urn:seaweedfs:s3",
    "spAcsUrl": "https://s3.example.com/saml/acs",
    "groupsAttribute": "http://schemas.xmlsoap.org/claims/Group",
    "roleMapping": {
      "rules": [
        {
          "claim": "http://schemas.xmlsoap.org/claims/Group",
          "value": "SeaweedFS-Admins",
          "role": "arn:aws:iam::role/AdfsS3Admin"
        }
      ],
      "defaultRole": "arn:aws:iam::role/AdfsS3ReadOnly"
    }
  }
}

Notes:

  • spAcsUrl must match the SAML endpoint URL configured on the ADFS relying party. ADFS stamps it into the assertion’s Recipient and the response’s Destination, and SeaweedFS verifies both, so in practice it is required for ADFS even though no browser flow terminates there.
  • The session name comes from the standard https://aws.amazon.com/SAML/Attributes/RoleSessionName assertion attribute when present, otherwise from the (sanitized) NameID.

In the roles’ trust policies, allow sts:AssumeRoleWithSAML for a Federated principal equal to the provider ARN, optionally pinned to the audience:

{
  "Effect": "Allow",
  "Principal": { "Federated": "arn:aws:iam::000000000000:saml-provider/adfs" },
  "Action": ["sts:AssumeRoleWithSAML"],
  "Condition": { "StringEquals": { "saml:aud": "urn:seaweedfs:s3" } }
}

Clients obtain a SAML assertion from ADFS — for example through the IdP-initiated sign-on page (/adfs/ls/IdpInitiatedSignOn.aspx, if enabled) or a scripted browser flow — then post it, base64-encoded, to the S3 endpoint:

curl -s https://s3.example.com/ \
  -d "Action=AssumeRoleWithSAML" \
  -d "Version=2011-06-15" \
  -d "RoleArn=arn:aws:iam::role/AdfsS3Admin" \
  -d "PrincipalArn=arn:aws:iam::000000000000:saml-provider/adfs" \
  --data-urlencode "SAMLAssertion=$(base64 < assertion.xml | tr -d '\n')"

As with the web-identity flow, the response carries temporary S3 credentials.

Troubleshooting

Symptom Likely cause Fix
Admin UI shows the username as a GUID or opaque sub value ADFS’s sub is a pairwise identifier, and no friendlier claim was found Set username_claim = "upn" and make sure an issuance rule emits UPN (grant allatclaims so it reaches the ID token)
Groups never map to a role Group claims missing from the token, or claim-name mismatch Verify the Token-Groups issuance rule exists; match groups_claim / rule claim to http://schemas.xmlsoap.org/claims/Group exactly, or add an ADFS rule emitting the claim as groups
Token rejected as expired or not yet valid right after login Clock skew between the ADFS servers and the SeaweedFS hosts Sync both sides with NTP; for the SAML path, clockSkewSeconds (default 120) widens the tolerance on assertion validity windows
AssumeRoleWithSAML rejects a valid assertion Recipient/Destination in the assertion don’t match the configured spAcsUrl, or the Audience doesn’t match spEntityId Set spAcsUrl to the exact SAML endpoint URL on the relying party; make the relying-party identifier equal spEntityId
OIDC discovery fails at admin startup ADFS TLS certificate from a private CA, or discovery URL unreachable Set tls_ca_cert to the CA bundle path; check the /adfs/.well-known/openid-configuration URL from the SeaweedFS host

For the full [admin.oidc] option list, role mapping semantics, and startup/TLS behavior, see the Admin UI OIDC Login reference.