Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OIDC Authentication, and Cloud CLI Contexts #1082

Open
wants to merge 19 commits into
base: master
Choose a base branch
from

Conversation

nickmisasi
Copy link
Contributor

@nickmisasi nickmisasi commented Oct 9, 2024

Summary

This PR introduces 2 major features

OIDC Authentication.

Defaulted to off, unless the server is provided with a Client ID, Secret, and Organization URL.
Setting the aforementioned parameters will result in the provisioner validating requests to /api/* contain a valid Access Token from Azure AD.
Authentication for CLI's is handled using Device Authorization grants, via cloud login command. Authentication data (access tokens, refresh tokens, etc) are stored in a context

Screenshots:
Login page of Azure AD (Browser auto-opens)
image

Logs in terminal when initiated:
image

CLI Contexts

Most users have their own custom hacks or bash scripts to automate the passing of --server to handle interacting with different environments. This PR introduces the concept of contexts, stored in ~/.cloud/contexts.json

Contexts store:

  • Server URL
  • Auth Data (like your access and refresh tokens)
  • An alias (for switching)
  • Client ID, and Org URL to kick off authorization flow (if configured)

A new CLI option now exists cloud contexts <command> with the following options:

Manipulate local contexts for the Cloud CLI

Usage:
  cloud contexts [command]

Available Commands:
  create      Create a new context.
  delete      Delete a context.
  get         Get details of a specific context.
  list        List all contexts.
  set-current Set the current context.
  update      Update a context.

Additionally, a cloud login command has been added, which allows you to retrigger the login flow for a particular context, should you run into authentication issues.

cloud login --help
Login to the Mattermost Cloud provisioning server.

Usage:
  cloud login [flags]

Flags:
      --context string   The name of the context to use.
  -h, --help             help for login

The --server parameter has been removed. Instead, use cloud context create to create a context for each Provisioner environment you interact with. To switch between them, use cloud context set-current. All cloud commands will initiate based off of the currently set context (todo: support --context to switch contexts for a single command)

On first run of the Cloud CLI with this change, the contexts.json file will be created for you, containing one context with alias local pointing at http://localhost:8075.

When creating a new context, if you provide a Client ID and OrgURL, the CLI will automatically initiate the login flow for you. To skip the authentication flow, pass the argument --skip-auth

Ticket Link

https://mattermost.atlassian.net/browse/CLD-8406

Release Note

Support for validating OAuth JWT tokens for authentication purposes
Addition of `contexts` command, to enable creating, saving, and switching between different provisioner environments

@mm-cloud-bot
Copy link

@nickmisasi: Adding the "do-not-merge/release-note-label-needed" label because no release-note block was detected, please follow our release note process to remove it.

I understand the commands that are listed here

@nickmisasi nickmisasi marked this pull request as ready for review October 25, 2024 17:31
@nickmisasi nickmisasi added the 2: Dev Review Requires review by a developer label Oct 25, 2024
@mm-cloud-bot mm-cloud-bot added release-note Denotes a PR that will be considered when it comes time to generate release notes. and removed do-not-merge/release-note-label-needed labels Oct 25, 2024
"strings"
"time"

"golang.org/x/oauth2"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Borrowed the OAuth2 built in types for some marshaling, but couldn't use it for token exchange here as MS requires a custom scope to be passed, and this library doesn't support doing that when using refresh tokens

@nickmisasi nickmisasi changed the title Draft: OIDC Authentication, and Cloud CLI Contexts OIDC Authentication, and Cloud CLI Contexts Oct 28, 2024
Copy link

@esarafianou esarafianou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First round of comments, but I'll re-review this.

internal/auth/device_authorization.go Outdated Show resolved Hide resolved
cmd/cloud/contexts.go Show resolved Hide resolved
cmd/cloud/main.go Show resolved Hide resolved
Comment on lines +34 to +39
newToken, err := Refresh(ctx, config, auth.RefreshToken)
if err != nil {
return nil, err
}

auth.AccessToken = newToken.AccessToken

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. At every refresh, we also get a new refresh token. While the previous one is still valid (that's a weird MS choice for a public OAuth2 client), we should stop using it and store the new refresh token.

Docs: https://learn.microsoft.com/en-us/entra/identity-platform/refresh-tokens#token-lifetime

  1. We also need to update the auth.ExpiresAt

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had left this intentionally as a "feature" to enforce re-authentications more frequently, but I guess we can defer that functionality to the auth side. I've changed it to match your suggestion but if you'd like me to change it back let me know.

Copy link

@esarafianou esarafianou Nov 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean. Can we actually revert the change with the refresh token? Apologies for that!

Reasoning:

  1. For public clients that are not single-page applications (SPAs), such as those utilizing the device authorization grant (device flow), the default refresh token lifetime is 90 days. This is already a lot for a public client but it's non-configurable.
  2. Each time a refresh token is used to acquire a new access token, Microsoft Entra ID issues a new refresh token, that inherits the same default lifetime as the original. As long as the refresh token is used within its valid period, it is continuously renewed, potentially allowing indefinite access without requiring the user to re-authenticate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted back to not using the new RefreshToken

internal/auth/device_authorization.go Show resolved Hide resolved
Comment on lines +174 to +175
command.Flags().StringSliceVar(&flags.RestrictedClientIDs, "restricted-client-ids", []string{}, "The list of restricted client IDs.")
command.Flags().StringSliceVar(&flags.RestrictedClientAllowedEndpointsList, "restricted-client-allowed-endpoints", []string{}, "The list of restricted endpoints.")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't we currently having a fixed list of restricted client IDs and restricted endpoints that we can have as a default config?

How is this command usually run? I'd like to avoid the human error of someone missing adding all the needed client IDs and restricted endpoints here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is run through IaC YAML manifests anywhere that the authentication would matter - see here we currently set allow-list-cidr-ranges and vpn-cidr ranges similarly to the way these would be set here, so I expect we should be ok from the human error side

model/client.go Show resolved Hide resolved
internal/api/middleware.go Show resolved Hide resolved
internal/api/middleware.go Show resolved Hide resolved
@nickmisasi
Copy link
Contributor Author

Getting rate limited on a build step. Will try again later

Copy link

@esarafianou esarafianou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 last comments, looks great otherwise!

internal/auth/cli.go Outdated Show resolved Hide resolved
model/client.go Outdated Show resolved Hide resolved
Copy link
Contributor

@gabrieljackson gabrieljackson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heya, have some initial feedback from my testing.

var contextsData Contexts
if _, statErr := os.Stat(contextFilePath); errors.Is(statErr, os.ErrNotExist) {
contextsData = bootstrapFirstContext()
WriteContexts(&contextsData)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are ignoring the return error here and this seems to be hiding an issue where bootstrapping will fail on a completely new bootstrap. I believe the issue is the .cloud directory isn't being created.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! This directory is created when the provisioner is run so I never ran into it. Fixed

Comment on lines 62 to 67
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, err
}

contextFilePath := filepath.Join(homeDir, ".cloud", "contexts.json")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's take this logic and put it into a reusable helper function to rule out any typos on the default context file path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2: Dev Review Requires review by a developer release-note Denotes a PR that will be considered when it comes time to generate release notes.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants