mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
feat: Add OIDC authentication (#3314)
* feat: Add OIDC authentication * Extract username into a separate package and add OIDC tests * Add test case for invalid tokens * Add test case for username as email * Add OIDC to the frontend * Improve comments from self-review * Add authentication docs * Add telemetry * Update docs/install/auth.md Co-authored-by: Ammar Bandukwala <ammar@ammar.io> * Update docs/install/auth.md Co-authored-by: Ammar Bandukwala <ammar@ammar.io> * Remove username package Co-authored-by: Ammar Bandukwala <ammar@ammar.io>
This commit is contained in:
@ -6,7 +6,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/google/go-github/v43/github"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/oauth2"
|
||||
@ -40,6 +42,7 @@ func (api *API) userAuthMethods(rw http.ResponseWriter, _ *http.Request) {
|
||||
httpapi.Write(rw, http.StatusOK, codersdk.AuthMethods{
|
||||
Password: true,
|
||||
Github: api.GithubOAuth2Config != nil,
|
||||
OIDC: api.OIDCConfig != nil,
|
||||
})
|
||||
}
|
||||
|
||||
@ -205,3 +208,137 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
http.Redirect(rw, r, redirect, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
type OIDCConfig struct {
|
||||
httpmw.OAuth2Config
|
||||
|
||||
Verifier *oidc.IDTokenVerifier
|
||||
// EmailDomain is the domain to enforce when a user authenticates.
|
||||
EmailDomain string
|
||||
AllowSignups bool
|
||||
}
|
||||
|
||||
func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
|
||||
state := httpmw.OAuth2(r)
|
||||
|
||||
// See the example here: https://github.com/coreos/go-oidc
|
||||
rawIDToken, ok := state.Token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "id_token not found in response payload. Ensure your OIDC callback is configured correctly!",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
idToken, err := api.OIDCConfig.Verifier.Verify(r.Context(), rawIDToken)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Failed to verify OIDC token.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var claims struct {
|
||||
Email string `json:"email"`
|
||||
Verified bool `json:"email_verified"`
|
||||
Username string `json:"preferred_username"`
|
||||
}
|
||||
err = idToken.Claims(&claims)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to extract OIDC claims.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if claims.Email == "" {
|
||||
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "No email found in OIDC payload!",
|
||||
})
|
||||
return
|
||||
}
|
||||
if !claims.Verified {
|
||||
httpapi.Write(rw, http.StatusForbidden, codersdk.Response{
|
||||
Message: fmt.Sprintf("Verify the %q email address on your OIDC provider to authenticate!", claims.Email),
|
||||
})
|
||||
return
|
||||
}
|
||||
// The username is a required property in Coder. We make a best-effort
|
||||
// attempt at using what the claims provide, but if that fails we will
|
||||
// generate a random username.
|
||||
if !httpapi.UsernameValid(claims.Username) {
|
||||
// If no username is provided, we can default to use the email address.
|
||||
// This will be converted in the from function below, so it's safe
|
||||
// to keep the domain.
|
||||
if claims.Username == "" {
|
||||
claims.Username = claims.Email
|
||||
}
|
||||
claims.Username = httpapi.UsernameFrom(claims.Username)
|
||||
}
|
||||
if api.OIDCConfig.EmailDomain != "" {
|
||||
if !strings.HasSuffix(claims.Email, api.OIDCConfig.EmailDomain) {
|
||||
httpapi.Write(rw, http.StatusForbidden, codersdk.Response{
|
||||
Message: fmt.Sprintf("Your email %q is not a part of the %q domain!", claims.Email, api.OIDCConfig.EmailDomain),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var user database.User
|
||||
user, err = api.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
|
||||
Email: claims.Email,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
if !api.OIDCConfig.AllowSignups {
|
||||
httpapi.Write(rw, http.StatusForbidden, codersdk.Response{
|
||||
Message: "Signups are disabled for OIDC authentication!",
|
||||
})
|
||||
return
|
||||
}
|
||||
var organizationID uuid.UUID
|
||||
organizations, _ := api.Database.GetOrganizations(r.Context())
|
||||
if len(organizations) > 0 {
|
||||
// Add the user to the first organization. Once multi-organization
|
||||
// support is added, we should enable a configuration map of user
|
||||
// email to organization.
|
||||
organizationID = organizations[0].ID
|
||||
}
|
||||
user, _, err = api.createUser(r.Context(), codersdk.CreateUserRequest{
|
||||
Email: claims.Email,
|
||||
Username: claims.Username,
|
||||
OrganizationID: organizationID,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error creating user.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to get user by email.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
_, created := api.createAPIKey(rw, r, database.InsertAPIKeyParams{
|
||||
UserID: user.ID,
|
||||
LoginType: database.LoginTypeOIDC,
|
||||
OAuthAccessToken: state.Token.AccessToken,
|
||||
OAuthRefreshToken: state.Token.RefreshToken,
|
||||
OAuthExpiry: state.Token.Expiry,
|
||||
})
|
||||
if !created {
|
||||
return
|
||||
}
|
||||
|
||||
redirect := state.Redirect
|
||||
if redirect == "" {
|
||||
redirect = "/"
|
||||
}
|
||||
http.Redirect(rw, r, redirect, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
Reference in New Issue
Block a user