MinIO's OpenID Connect Integration Explained

MinIO provides a flexible Identity and Access Management system that can be integrated with popular external identity providers. MinIO IAM is built with AWS IAM compatibility at its core - access is controlled by policies mirroring AWS' IAM policies. While AWS supports myriad ways to control access, including ACLs, Bucket Policies, etc, in the interest of simplicity, MinIO's access control is based on policies associated with users or user groups.

In this context, a policy is an identifier for a JSON document that follows the same syntax as AWS's access policy documents and has statements about the allowed (and forbidden) APIs, resources, and conditions of access. Object storage access by an entity is controlled by its identity and the access policies applied to this identity.

MinIO server has a built-in internal identity provider that supports creating users and user groups, attaching one or more policies to these users or groups and creating access key pairs with optional expiry and reduced-scope policies.

To integrate with external authentication and authorization systems, MinIO provides extensions to the Security Token Service (STS) APIs to work with OpenID Connect-based systems, Active Directory/LDAP-based systems, Client TLS Certificate-based systems, and also includes support for plugging in arbitrary external authorization and authentication systems. In this post, we will focus on feature support for the widely popular OpenID Connect (OIDC) standards-based external IDentity Providers (IDPs).

Plugging Into an Organization's OpenID Connect-based Single-Sign-On System

The simplest usage of OIDC with MinIO is to integrate with your organization's IDP. In this scenario, MinIO delegates authentication to an OIDC-compatible service. Once authentication succeeds, the IDP generates an id_token - this is a JSON Web Token (JWT) containing a cryptographic proof of authentication. MinIO verifies the authentication done by the IDP using the cryptographic signature in the JWT using the public key of the IDP.

Any client application can perform authentication for an organization user with the OIDC server and then call MinIO's AssumeRoleWithWebIdentity STS API providing it with an id_token received from the OIDC service, to generate temporary credentials to access object storage.

The most common such client application is the MinIO server's Web Console. The Console implements authentication with the OpenID Connect's Authorization Code Flow and uses the received id_token token in a subsequent STS API call to generate temporary credentials for object storage access. When logging in to the Console, the user is redirected to the organization's IDP to enter their credentials and complete authentication (potentially including MFA-based authentication steps), and then they are dropped into the MinIO Console interface to interact with object storage.

What about authorization to access object storage? MinIO supports two approaches here that we call Custom id_token Claim and Role Policy.

Custom id_token Claim

The first (and older) approach uses information in claims provided in the id_token received from the external IDP. An organization may configure a custom claim in the IDP to list the names of the policies to be applied to a user. As an example, the decoded id_token below (from a development environment) shows the value for a custom claim called "groups". With this custom claim configured in the MinIO server, the value is interpreted as a list of policy names to be applied to the user.

{
  "alg": "RS256",
  "kid": "4d7a8d4a74c6f1443890ddba7c3b500efd06e8c4"
}.{
  "iss": "http://127.0.0.1:5556/dex",
  "sub": "Cit1aWQ9ZGlsbG9uLG91PXBlb3BsZSxvdT1zd2VuZ2csZGM9bWluLGRjPWlvEgRsZGFw",
  "aud": "minio-client-app",
  "exp": 1697571883,
  "iat": 1697485483,
  "at_hash": "cITlbF83_r4rU-fLWWWJeA",
  "c_hash": "EYUT2tpGQh4gB_AXIHH6Pw",
  "email": "johndoe@example.io",
  "groups": [
    "projecta",
    "projectb"
  ]
}.[Signature]

The two policies projecta and projectb define the maximum scope of access that this user (and any access keys they generate) may have. The JSON policy document for these policies may make use of additional properties in the id_token via policy variables to further customize the policy with various conditions (described later in this post) as documented in OpenID Connect Access Management — MinIO Object Storage for Kubernetes.

While simple, the chief limitation of this approach is that administering this system involves configuring the organization's IDP with the custom claim and setting it up for different projects and users. We found that for a lot of organizations, the team managing the MinIO deployments and the team managing the IDP are distinct and the process involved a lot of back-and-forth in setting up and configuring this custom claim for each use-case.

Role Policies

The second, more flexible approach to managing authorization to object storage allows configuring all access policies for the IDP in the MinIO server itself. This removes the need for configuring the external IDP with a custom claim and enables additional use cases, especially when the organization does not want to or cannot modify the claims returned by the IDP.

In this approach an external IDP defines a "role" - i.e. a set of policies that apply to any user authenticated by this IDP. When configuring the external IDP, the administrator lists the policies that should apply to this external IDP as the role_policy. The MinIO server assigns a unique value called the RoleARN to this configuration. This value is provided by the client to the MinIO server in the AssumeRoleWithWebIdentity STS API call to generate temporary credentials.

With a role policy, all users get the same list of policies applied. So how can we assign users or groups of users different access policies? The answer is policy variables. The id_token contains properties of the user such as their email address, unique ID and attributes such as organization internal groups. We can make use of this info to restrict access. Let's look at an example of this.

Consider the decoded id_token in the previous section. It represents a user whose email address is johndoe@example.io - this user is also a member of a couple of groups. To assign a policy specific to members of groups projecta and projectb, we can create policies with content like the below:

Policy "projecta"

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": [
                "arn:aws:s3:::projecta/",
                "arn:aws:s3:::projecta/*"
            ],
            "Condition": {
                "ForAnyValue:StringEquals": {"jwt:groups": "projecta"}
            }
        }
    ]
}

Policy "projectb"

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": [
                "arn:aws:s3:::projectb/",
                "arn:aws:s3:::projectb/*"
            ],
            "Condition": {
                "ForAnyValue:StringEquals": {"jwt:groups": "projectb"}
            }
        }
    ]
}

Each of these policies allows all S3 actions on specific buckets (also called projecta and projectb). They also include a condition on the jwt:groups property specifying that the statement applies only to requests made by users whose groups list includes the given group name. Since the jwt:groups property is multi-valued (i.e. a user may be a member of multiple groups), the ForAnyValue: qualifier is used to indicate that the StringEquals function should match against any of the groups in the list.

As a final example, if we want to give certain users access to all S3 buckets, let's say identified by their email address, we can create a separate policy like so:

Policy "allbuckets"

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": [
                "arn:aws:s3:::*"
            ],
            "Condition": {
                "StringEquals": {
                    "jwt:email": [
                        "johndoe@example.io",
                        "janedoe@example.io"
                    ]
                }
            }
        }
    ]
}

In this case, the jwt:email is a single-valued property that is matched (with an OR logic) against each of the given email addresses in the policy.

When configuring MinIO, all the above three policies would be listed in the role_policy parameter for the IDP. Only users with id_token info matching the policy conditions will have the policy applied and thus gain access to object storage.

Using Multiple OIDC Identity Providers

MinIO allows configuring multiple IDPs with role policies configured - as each request to the STS API requires the RoleARN to be specified - there is no ambiguity for the MinIO server in validating the id_tokens against the appropriate provider and processing the request.

This enables new application-specific use cases. Suppose we want to develop a mobile application with the following (not uncommon) requirements:

  • user-specific application data (photos, videos and documents for example) needs to be stored in object storage
  • each user's data needs to be stored under a unique path in object storage
  • we do not want to save object storage credentials in the application binary or on the user's device
  • users should be able to authenticate themselves with certain common OpenID providers, say Google, Facebook and GitHub

As we know, dynamic, temporary credentials for object storage can be generated with the AssumeRoleWithWebIdentity STS API. MinIO can be configured with the three mentioned OpenID providers using role policies so that users can be authenticated via their id_tokens. The policy for access is based on policy variables and provides unique paths for each user based on a unique identifier:

{
"Version": "2012-10-17",
"Statement": [
      {
         "Action": ["s3:ListBucket"],
         "Effect": "Allow",
         "Resource": ["arn:aws:s3:::mybucket"],
         "Condition": {"StringLike": {"s3:prefix": ["github/${jwt:upn}/*"]}}
      },
      {
         "Action": [
         "s3:GetObject",
         "s3:PutObject"
         ],
         "Effect": "Allow",
         "Resource": ["arn:aws:s3:::mybucket/github/${jwt:upn}/*"]
      }
   ]
}

The above shows an example policy that could be used with GitHub as the external IDP. The jwt:upn is the "user principal name" claim in the id_token and would be unique for each user - any such unique identifier provided by the IDP can be used here. Note that the path is prefixed by the provider name to ensure uniqueness.

MinIO's OIDC Integration - a flexible solution without compromises

By leveraging OIDC standards, MinIO seamlessly interfaces with external IDPs, streamlining the authentication process and ensuring robust cryptographic verification. Using either custom id_token claims or role policies, organizations are empowered to enforce finely-tuned access controls, tailoring them according to user attributes and group affiliations. The capability to configure multiple IDPs with minimum configuration underscores MinIO's adaptability, enabling a diverse range of applications to create dynamic storage solutions without compromising on security. It stands as a testament to the platform's commitment to user convenience and security in managing object storage.

You can download MinIO here. If you have any questions, ping us at hello@min.io or join the Slack community.