mirror of
https://github.com/coder/coder.git
synced 2025-07-21 01:28:49 +00:00
feat: allow external services to be authable (#9996)
* feat: allow external services to be authable * Refactor external auth config structure for defaults * Add support for new config properties * Change the name of external auth * Move externalauth -> external-auth * Run gen * Fix tests * Fix MW tests * Fix git auth redirect * Fix lint * Fix name * Allow any ID * Fix invalid type test * Fix e2e tests * Fix comments * Fix colors * Allow accepting any type as string * Run gen * Fix href
This commit is contained in:
152
coderd/apidoc/docs.go
generated
152
coderd/apidoc/docs.go
generated
@ -602,7 +602,7 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/externalauth/{externalauth}": {
|
||||
"/external-auth/{externalauth}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
@ -637,7 +637,7 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/externalauth/{externalauth}/device": {
|
||||
"/external-auth/{externalauth}/device": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
@ -2768,7 +2768,7 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/externalauth": {
|
||||
"/templateversions/{templateversion}/external-auth": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
@ -6725,13 +6725,13 @@ const docTemplate = `{
|
||||
"clibase.Regexp": {
|
||||
"type": "object"
|
||||
},
|
||||
"clibase.Struct-array_codersdk_GitAuthConfig": {
|
||||
"clibase.Struct-array_codersdk_ExternalAuthConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.GitAuthConfig"
|
||||
"$ref": "#/definitions/codersdk.ExternalAuthConfig"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7978,15 +7978,15 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"external_auth": {
|
||||
"$ref": "#/definitions/clibase.Struct-array_codersdk_ExternalAuthConfig"
|
||||
},
|
||||
"external_token_encryption_keys": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"git_auth": {
|
||||
"$ref": "#/definitions/clibase.Struct-array_codersdk_GitAuthConfig"
|
||||
},
|
||||
"http_address": {
|
||||
"description": "HTTPAddress is a string because it may be set to zero to disable.",
|
||||
"type": "string"
|
||||
@ -8203,6 +8203,9 @@ const docTemplate = `{
|
||||
"device": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"installations": {
|
||||
"description": "AppInstallations are the installations that the user has access to.",
|
||||
"type": "array",
|
||||
@ -8210,9 +8213,6 @@ const docTemplate = `{
|
||||
"$ref": "#/definitions/codersdk.ExternalAuthAppInstallation"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"user": {
|
||||
"description": "User is the user that authenticated with the provider.",
|
||||
"allOf": [
|
||||
@ -8237,6 +8237,64 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ExternalAuthConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"app_install_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"app_installations_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"auth_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"client_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"device_code_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"device_flow": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"display_icon": {
|
||||
"description": "DisplayIcon is a URL to an icon to display in the UI.",
|
||||
"type": "string"
|
||||
},
|
||||
"display_name": {
|
||||
"description": "DisplayName is shown in the UI to identify the auth config.",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "ID is a unique identifier for the auth config.\nIt defaults to ` + "`" + `type` + "`" + ` when not provided.",
|
||||
"type": "string"
|
||||
},
|
||||
"no_refresh": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"regex": {
|
||||
"description": "Regex allows API requesters to match an auth config by\na string (e.g. coder.com) instead of by it's type.\n\nGit clone makes use of this by parsing the URL from:\n'Username for \"https://github.com\":'\nAnd sending it to the Coder server to match against the Regex.",
|
||||
"type": "string"
|
||||
},
|
||||
"scopes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"token_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "Type is the type of external auth config.",
|
||||
"type": "string"
|
||||
},
|
||||
"validate_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ExternalAuthDevice": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -8257,23 +8315,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ExternalAuthProvider": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"azure-devops",
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"openid-connect"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"ExternalAuthProviderAzureDevops",
|
||||
"ExternalAuthProviderGitHub",
|
||||
"ExternalAuthProviderGitLab",
|
||||
"ExternalAuthProviderBitBucket",
|
||||
"ExternalAuthProviderOpenIDConnect"
|
||||
]
|
||||
},
|
||||
"codersdk.ExternalAuthUser": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -8330,53 +8371,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.GitAuthConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"app_install_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"app_installations_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"auth_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"client_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"device_code_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"device_flow": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"no_refresh": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"regex": {
|
||||
"type": "string"
|
||||
},
|
||||
"scopes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"token_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"validate_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.GitSSHKey": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -10018,11 +10012,17 @@ const docTemplate = `{
|
||||
"authenticated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"display_icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/codersdk.ExternalAuthProvider"
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
152
coderd/apidoc/swagger.json
generated
152
coderd/apidoc/swagger.json
generated
@ -512,7 +512,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/externalauth/{externalauth}": {
|
||||
"/external-auth/{externalauth}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
@ -543,7 +543,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/externalauth/{externalauth}/device": {
|
||||
"/external-auth/{externalauth}/device": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
@ -2430,7 +2430,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/externalauth": {
|
||||
"/templateversions/{templateversion}/external-auth": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
@ -5961,13 +5961,13 @@
|
||||
"clibase.Regexp": {
|
||||
"type": "object"
|
||||
},
|
||||
"clibase.Struct-array_codersdk_GitAuthConfig": {
|
||||
"clibase.Struct-array_codersdk_ExternalAuthConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.GitAuthConfig"
|
||||
"$ref": "#/definitions/codersdk.ExternalAuthConfig"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7130,15 +7130,15 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"external_auth": {
|
||||
"$ref": "#/definitions/clibase.Struct-array_codersdk_ExternalAuthConfig"
|
||||
},
|
||||
"external_token_encryption_keys": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"git_auth": {
|
||||
"$ref": "#/definitions/clibase.Struct-array_codersdk_GitAuthConfig"
|
||||
},
|
||||
"http_address": {
|
||||
"description": "HTTPAddress is a string because it may be set to zero to disable.",
|
||||
"type": "string"
|
||||
@ -7351,6 +7351,9 @@
|
||||
"device": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"installations": {
|
||||
"description": "AppInstallations are the installations that the user has access to.",
|
||||
"type": "array",
|
||||
@ -7358,9 +7361,6 @@
|
||||
"$ref": "#/definitions/codersdk.ExternalAuthAppInstallation"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"user": {
|
||||
"description": "User is the user that authenticated with the provider.",
|
||||
"allOf": [
|
||||
@ -7385,6 +7385,64 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ExternalAuthConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"app_install_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"app_installations_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"auth_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"client_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"device_code_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"device_flow": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"display_icon": {
|
||||
"description": "DisplayIcon is a URL to an icon to display in the UI.",
|
||||
"type": "string"
|
||||
},
|
||||
"display_name": {
|
||||
"description": "DisplayName is shown in the UI to identify the auth config.",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "ID is a unique identifier for the auth config.\nIt defaults to `type` when not provided.",
|
||||
"type": "string"
|
||||
},
|
||||
"no_refresh": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"regex": {
|
||||
"description": "Regex allows API requesters to match an auth config by\na string (e.g. coder.com) instead of by it's type.\n\nGit clone makes use of this by parsing the URL from:\n'Username for \"https://github.com\":'\nAnd sending it to the Coder server to match against the Regex.",
|
||||
"type": "string"
|
||||
},
|
||||
"scopes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"token_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "Type is the type of external auth config.",
|
||||
"type": "string"
|
||||
},
|
||||
"validate_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ExternalAuthDevice": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -7405,23 +7463,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ExternalAuthProvider": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"azure-devops",
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"openid-connect"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"ExternalAuthProviderAzureDevops",
|
||||
"ExternalAuthProviderGitHub",
|
||||
"ExternalAuthProviderGitLab",
|
||||
"ExternalAuthProviderBitBucket",
|
||||
"ExternalAuthProviderOpenIDConnect"
|
||||
]
|
||||
},
|
||||
"codersdk.ExternalAuthUser": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -7478,53 +7519,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.GitAuthConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"app_install_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"app_installations_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"auth_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"client_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"device_code_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"device_flow": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"no_refresh": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"regex": {
|
||||
"type": "string"
|
||||
},
|
||||
"scopes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"token_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"validate_url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.GitSSHKey": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -9065,11 +9059,17 @@
|
||||
"authenticated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"display_icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/codersdk.ExternalAuthProvider"
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -547,7 +547,7 @@ func New(options *Options) *API {
|
||||
|
||||
// Register callback handlers for each OAuth2 provider.
|
||||
// We must support gitauth and externalauth for backwards compatibility.
|
||||
for _, route := range []string{"gitauth", "externalauth"} {
|
||||
for _, route := range []string{"gitauth", "external-auth"} {
|
||||
r.Route("/"+route, func(r chi.Router) {
|
||||
for _, externalAuthConfig := range options.ExternalAuthConfigs {
|
||||
// We don't need to register a callback handler for device auth.
|
||||
@ -616,7 +616,7 @@ func New(options *Options) *API {
|
||||
r.Get("/{fileID}", api.fileByID)
|
||||
r.Post("/", api.postFile)
|
||||
})
|
||||
r.Route("/externalauth/{externalauth}", func(r chi.Router) {
|
||||
r.Route("/external-auth/{externalauth}", func(r chi.Router) {
|
||||
r.Use(
|
||||
apiKeyMiddleware,
|
||||
httpmw.ExtractExternalAuthParam(options.ExternalAuthConfigs),
|
||||
@ -689,7 +689,7 @@ func New(options *Options) *API {
|
||||
r.Get("/schema", templateVersionSchemaDeprecated)
|
||||
r.Get("/parameters", templateVersionParametersDeprecated)
|
||||
r.Get("/rich-parameters", api.templateVersionRichParameters)
|
||||
r.Get("/externalauth", api.templateVersionExternalAuth)
|
||||
r.Get("/external-auth", api.templateVersionExternalAuth)
|
||||
r.Get("/variables", api.templateVersionVariables)
|
||||
r.Get("/resources", api.templateVersionResources)
|
||||
r.Get("/logs", api.templateVersionLogs)
|
||||
|
@ -906,7 +906,7 @@ func RequestExternalAuthCallback(t *testing.T, providerID string, client *coders
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
state := "somestate"
|
||||
oauthURL, err := client.URL.Parse(fmt.Sprintf("/externalauth/%s/callback?code=asd&state=%s", providerID, state))
|
||||
oauthURL, err := client.URL.Parse(fmt.Sprintf("/external-auth/%s/callback?code=asd&state=%s", providerID, state))
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil)
|
||||
require.NoError(t, err)
|
||||
|
2
coderd/database/dump.sql
generated
2
coderd/database/dump.sql
generated
@ -643,7 +643,7 @@ CREATE TABLE template_versions (
|
||||
message character varying(1048576) DEFAULT ''::character varying NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN template_versions.external_auth_providers IS 'IDs of Git auth providers for a specific template version';
|
||||
COMMENT ON COLUMN template_versions.external_auth_providers IS 'IDs of External auth providers for a specific template version';
|
||||
|
||||
COMMENT ON COLUMN template_versions.message IS 'Message describing the changes in this version of the template, similar to a Git commit message. Like a commit message, this should be a short, high-level description of the changes in this version of the template. This message is immutable and should not be updated after the fact.';
|
||||
|
||||
|
@ -22,4 +22,6 @@ FROM
|
||||
|
||||
COMMENT ON VIEW template_version_with_user IS 'Joins in the username + avatar url of the created by user.';
|
||||
|
||||
COMMENT ON COLUMN template_versions.external_auth_providers IS 'IDs of External auth providers for a specific template version';
|
||||
|
||||
COMMIT;
|
||||
|
@ -1857,7 +1857,7 @@ type TemplateVersionTable struct {
|
||||
Readme string `db:"readme" json:"readme"`
|
||||
JobID uuid.UUID `db:"job_id" json:"job_id"`
|
||||
CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
|
||||
// IDs of Git auth providers for a specific template version
|
||||
// IDs of External auth providers for a specific template version
|
||||
ExternalAuthProviders []string `db:"external_auth_providers" json:"external_auth_providers"`
|
||||
// Message describing the changes in this version of the template, similar to a Git commit message. Like a commit message, this should be a short, high-level description of the changes in this version of the template. This message is immutable and should not be updated after the fact.
|
||||
Message string `db:"message" json:"message"`
|
||||
|
@ -23,7 +23,7 @@ import (
|
||||
// @Tags Git
|
||||
// @Param externalauth path string true "Git Provider ID" format(string)
|
||||
// @Success 200 {object} codersdk.ExternalAuth
|
||||
// @Router /externalauth/{externalauth} [get]
|
||||
// @Router /external-auth/{externalauth} [get]
|
||||
func (api *API) externalAuthByID(w http.ResponseWriter, r *http.Request) {
|
||||
config := httpmw.ExternalAuthParam(r)
|
||||
apiKey := httpmw.APIKey(r)
|
||||
@ -33,7 +33,7 @@ func (api *API) externalAuthByID(w http.ResponseWriter, r *http.Request) {
|
||||
Authenticated: false,
|
||||
Device: config.DeviceAuth != nil,
|
||||
AppInstallURL: config.AppInstallURL,
|
||||
Type: config.Type.Pretty(),
|
||||
DisplayName: config.DisplayName,
|
||||
AppInstallations: []codersdk.ExternalAuthAppInstallation{},
|
||||
}
|
||||
|
||||
@ -82,7 +82,7 @@ func (api *API) externalAuthByID(w http.ResponseWriter, r *http.Request) {
|
||||
// @Tags Git
|
||||
// @Param externalauth path string true "External Provider ID" format(string)
|
||||
// @Success 204
|
||||
// @Router /externalauth/{externalauth}/device [post]
|
||||
// @Router /external-auth/{externalauth}/device [post]
|
||||
func (api *API) postExternalAuthDeviceByID(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
apiKey := httpmw.APIKey(r)
|
||||
@ -169,7 +169,7 @@ func (api *API) postExternalAuthDeviceByID(rw http.ResponseWriter, r *http.Reque
|
||||
// @Tags Git
|
||||
// @Param externalauth path string true "Git Provider ID" format(string)
|
||||
// @Success 200 {object} codersdk.ExternalAuthDevice
|
||||
// @Router /externalauth/{externalauth}/device [get]
|
||||
// @Router /external-auth/{externalauth}/device [get]
|
||||
func (*API) externalAuthDeviceByID(rw http.ResponseWriter, r *http.Request) {
|
||||
config := httpmw.ExternalAuthParam(r)
|
||||
ctx := r.Context()
|
||||
@ -255,7 +255,7 @@ func (api *API) externalAuthCallback(externalAuthConfig *externalauth.Config) ht
|
||||
redirect := state.Redirect
|
||||
if redirect == "" {
|
||||
// This is a nicely rendered screen on the frontend
|
||||
redirect = fmt.Sprintf("/externalauth/%s", externalAuthConfig.ID)
|
||||
redirect = fmt.Sprintf("/external-auth/%s", externalAuthConfig.ID)
|
||||
}
|
||||
http.Redirect(rw, r, redirect, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/google/go-github/v43/github"
|
||||
xgithub "golang.org/x/oauth2/github"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
@ -35,9 +36,13 @@ type Config struct {
|
||||
// ID is a unique identifier for the authenticator.
|
||||
ID string
|
||||
// Type is the type of provider.
|
||||
Type codersdk.ExternalAuthProvider
|
||||
Type string
|
||||
// DeviceAuth is set if the provider uses the device flow.
|
||||
DeviceAuth *DeviceAuth
|
||||
// DisplayName is the name of the provider to display to the user.
|
||||
DisplayName string
|
||||
// DisplayIcon is the path to an image that will be displayed to the user.
|
||||
DisplayIcon string
|
||||
|
||||
// NoRefresh stops Coder from using the refresh token
|
||||
// to renew the access token.
|
||||
@ -113,7 +118,7 @@ validate:
|
||||
// to the read replica in time.
|
||||
//
|
||||
// We do an exponential backoff here to give the write time to propagate.
|
||||
if c.Type == codersdk.ExternalAuthProviderGitHub && r.Wait(retryCtx) {
|
||||
if c.Type == string(codersdk.EnhancedExternalAuthProviderGitHub) && r.Wait(retryCtx) {
|
||||
goto validate
|
||||
}
|
||||
// The token is no longer valid!
|
||||
@ -171,7 +176,7 @@ func (c *Config) ValidateToken(ctx context.Context, token string) (bool, *coders
|
||||
}
|
||||
|
||||
var user *codersdk.ExternalAuthUser
|
||||
if c.Type == codersdk.ExternalAuthProviderGitHub {
|
||||
if c.Type == string(codersdk.EnhancedExternalAuthProviderGitHub) {
|
||||
var ghUser github.User
|
||||
err = json.NewDecoder(res.Body).Decode(&ghUser)
|
||||
if err == nil {
|
||||
@ -217,7 +222,7 @@ func (c *Config) AppInstallations(ctx context.Context, token string) ([]codersdk
|
||||
return nil, false, nil
|
||||
}
|
||||
installs := []codersdk.ExternalAuthAppInstallation{}
|
||||
if c.Type == codersdk.ExternalAuthProviderGitHub {
|
||||
if c.Type == string(codersdk.EnhancedExternalAuthProviderGitHub) {
|
||||
var ghInstalls struct {
|
||||
Installations []*github.Installation `json:"installations"`
|
||||
}
|
||||
@ -245,50 +250,158 @@ func (c *Config) AppInstallations(ctx context.Context, token string) ([]codersdk
|
||||
return installs, true, nil
|
||||
}
|
||||
|
||||
type DeviceAuth struct {
|
||||
ClientID string
|
||||
TokenURL string
|
||||
Scopes []string
|
||||
CodeURL string
|
||||
}
|
||||
|
||||
// AuthorizeDevice begins the device authorization flow.
|
||||
// See: https://tools.ietf.org/html/rfc8628#section-3.1
|
||||
func (c *DeviceAuth) AuthorizeDevice(ctx context.Context) (*codersdk.ExternalAuthDevice, error) {
|
||||
if c.CodeURL == "" {
|
||||
return nil, xerrors.New("oauth2: device code URL not set")
|
||||
}
|
||||
codeURL, err := c.formatDeviceCodeURL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, codeURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var r struct {
|
||||
codersdk.ExternalAuthDevice
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
err = json.NewDecoder(resp.Body).Decode(&r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.ErrorDescription != "" {
|
||||
return nil, xerrors.New(r.ErrorDescription)
|
||||
}
|
||||
return &r.ExternalAuthDevice, nil
|
||||
}
|
||||
|
||||
type ExchangeDeviceCodeResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
|
||||
// ExchangeDeviceCode exchanges a device code for an access token.
|
||||
// The boolean returned indicates whether the device code is still pending
|
||||
// and the caller should try again.
|
||||
func (c *DeviceAuth) ExchangeDeviceCode(ctx context.Context, deviceCode string) (*oauth2.Token, error) {
|
||||
if c.TokenURL == "" {
|
||||
return nil, xerrors.New("oauth2: token URL not set")
|
||||
}
|
||||
tokenURL, err := c.formatDeviceTokenURL(deviceCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, codersdk.ReadBodyAsError(resp)
|
||||
}
|
||||
var body ExchangeDeviceCodeResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if body.Error != "" {
|
||||
return nil, xerrors.New(body.Error)
|
||||
}
|
||||
return &oauth2.Token{
|
||||
AccessToken: body.AccessToken,
|
||||
RefreshToken: body.RefreshToken,
|
||||
Expiry: dbtime.Now().Add(time.Duration(body.ExpiresIn) * time.Second),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *DeviceAuth) formatDeviceTokenURL(deviceCode string) (string, error) {
|
||||
tok, err := url.Parse(c.TokenURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tok.RawQuery = url.Values{
|
||||
"client_id": {c.ClientID},
|
||||
"device_code": {deviceCode},
|
||||
"grant_type": {"urn:ietf:params:oauth:grant-type:device_code"},
|
||||
}.Encode()
|
||||
return tok.String(), nil
|
||||
}
|
||||
|
||||
func (c *DeviceAuth) formatDeviceCodeURL() (string, error) {
|
||||
cod, err := url.Parse(c.CodeURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
cod.RawQuery = url.Values{
|
||||
"client_id": {c.ClientID},
|
||||
"scope": c.Scopes,
|
||||
}.Encode()
|
||||
return cod.String(), nil
|
||||
}
|
||||
|
||||
// ConvertConfig converts the SDK configuration entry format
|
||||
// to the parsed and ready-to-consume in coderd provider type.
|
||||
func ConvertConfig(entries []codersdk.GitAuthConfig, accessURL *url.URL) ([]*Config, error) {
|
||||
func ConvertConfig(entries []codersdk.ExternalAuthConfig, accessURL *url.URL) ([]*Config, error) {
|
||||
ids := map[string]struct{}{}
|
||||
configs := []*Config{}
|
||||
for _, entry := range entries {
|
||||
var typ codersdk.ExternalAuthProvider
|
||||
switch codersdk.ExternalAuthProvider(entry.Type) {
|
||||
case codersdk.ExternalAuthProviderAzureDevops:
|
||||
typ = codersdk.ExternalAuthProviderAzureDevops
|
||||
case codersdk.ExternalAuthProviderBitBucket:
|
||||
typ = codersdk.ExternalAuthProviderBitBucket
|
||||
case codersdk.ExternalAuthProviderGitHub:
|
||||
typ = codersdk.ExternalAuthProviderGitHub
|
||||
case codersdk.ExternalAuthProviderGitLab:
|
||||
typ = codersdk.ExternalAuthProviderGitLab
|
||||
default:
|
||||
return nil, xerrors.Errorf("unknown git provider type: %q", entry.Type)
|
||||
}
|
||||
if entry.ID == "" {
|
||||
// Default to the type.
|
||||
entry.ID = string(typ)
|
||||
}
|
||||
if valid := httpapi.NameValid(entry.ID); valid != nil {
|
||||
entry := entry
|
||||
|
||||
// Applies defaults to the config entry.
|
||||
// This allows users to very simply state that they type is "GitHub",
|
||||
// apply their client secret and ID, and have the UI appear nicely.
|
||||
applyDefaultsToConfig(&entry)
|
||||
|
||||
valid := httpapi.NameValid(entry.ID)
|
||||
if valid != nil {
|
||||
return nil, xerrors.Errorf("external auth provider %q doesn't have a valid id: %w", entry.ID, valid)
|
||||
}
|
||||
if entry.ClientID == "" {
|
||||
return nil, xerrors.Errorf("%q external auth provider: client_id must be provided", entry.ID)
|
||||
}
|
||||
if entry.ClientSecret == "" {
|
||||
return nil, xerrors.Errorf("%q external auth provider: client_secret must be provided", entry.ID)
|
||||
}
|
||||
|
||||
_, exists := ids[entry.ID]
|
||||
if exists {
|
||||
if entry.ID == string(typ) {
|
||||
return nil, xerrors.Errorf("multiple %s external auth providers provided. you must specify a unique id for each", typ)
|
||||
if entry.ID == entry.Type {
|
||||
return nil, xerrors.Errorf("multiple %s external auth providers provided. you must specify a unique id for each", entry.Type)
|
||||
}
|
||||
return nil, xerrors.Errorf("multiple git providers exist with the id %q. specify a unique id for each", entry.ID)
|
||||
return nil, xerrors.Errorf("multiple external auth providers exist with the id %q. specify a unique id for each", entry.ID)
|
||||
}
|
||||
ids[entry.ID] = struct{}{}
|
||||
|
||||
if entry.ClientID == "" {
|
||||
return nil, xerrors.Errorf("%q external auth provider: client_id must be provided", entry.ID)
|
||||
}
|
||||
authRedirect, err := accessURL.Parse(fmt.Sprintf("/externalauth/%s/callback", entry.ID))
|
||||
authRedirect, err := accessURL.Parse(fmt.Sprintf("/external-auth/%s/callback", entry.ID))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("parse externalauth callback url: %w", err)
|
||||
return nil, xerrors.Errorf("parse external auth callback url: %w", err)
|
||||
}
|
||||
regex := regex[typ]
|
||||
|
||||
var regex *regexp.Regexp
|
||||
if entry.Regex != "" {
|
||||
regex, err = regexp.Compile(entry.Regex)
|
||||
if err != nil {
|
||||
@ -299,30 +412,17 @@ func ConvertConfig(entries []codersdk.GitAuthConfig, accessURL *url.URL) ([]*Con
|
||||
oc := &oauth2.Config{
|
||||
ClientID: entry.ClientID,
|
||||
ClientSecret: entry.ClientSecret,
|
||||
Endpoint: endpoint[typ],
|
||||
RedirectURL: authRedirect.String(),
|
||||
Scopes: scope[typ],
|
||||
}
|
||||
|
||||
if entry.AuthURL != "" {
|
||||
oc.Endpoint.AuthURL = entry.AuthURL
|
||||
}
|
||||
if entry.TokenURL != "" {
|
||||
oc.Endpoint.TokenURL = entry.TokenURL
|
||||
}
|
||||
if entry.Scopes != nil && len(entry.Scopes) > 0 {
|
||||
oc.Scopes = entry.Scopes
|
||||
}
|
||||
if entry.ValidateURL == "" {
|
||||
entry.ValidateURL = validateURL[typ]
|
||||
}
|
||||
if entry.AppInstallationsURL == "" {
|
||||
entry.AppInstallationsURL = appInstallationsURL[typ]
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: entry.AuthURL,
|
||||
TokenURL: entry.TokenURL,
|
||||
},
|
||||
RedirectURL: authRedirect.String(),
|
||||
Scopes: entry.Scopes,
|
||||
}
|
||||
|
||||
var oauthConfig OAuth2Config = oc
|
||||
// Azure DevOps uses JWT token authentication!
|
||||
if typ == codersdk.ExternalAuthProviderAzureDevops {
|
||||
if entry.Type == string(codersdk.EnhancedExternalAuthProviderAzureDevops) {
|
||||
oauthConfig = &jwtConfig{oc}
|
||||
}
|
||||
|
||||
@ -330,17 +430,16 @@ func ConvertConfig(entries []codersdk.GitAuthConfig, accessURL *url.URL) ([]*Con
|
||||
OAuth2Config: oauthConfig,
|
||||
ID: entry.ID,
|
||||
Regex: regex,
|
||||
Type: typ,
|
||||
Type: entry.Type,
|
||||
NoRefresh: entry.NoRefresh,
|
||||
ValidateURL: entry.ValidateURL,
|
||||
AppInstallationsURL: entry.AppInstallationsURL,
|
||||
AppInstallURL: entry.AppInstallURL,
|
||||
DisplayName: entry.DisplayName,
|
||||
DisplayIcon: entry.DisplayIcon,
|
||||
}
|
||||
|
||||
if entry.DeviceFlow {
|
||||
if entry.DeviceCodeURL == "" {
|
||||
entry.DeviceCodeURL = deviceAuthURL[typ]
|
||||
}
|
||||
if entry.DeviceCodeURL == "" {
|
||||
return nil, xerrors.Errorf("external auth provider %q: device auth url must be provided", entry.ID)
|
||||
}
|
||||
@ -356,3 +455,123 @@ func ConvertConfig(entries []codersdk.GitAuthConfig, accessURL *url.URL) ([]*Con
|
||||
}
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// applyDefaultsToConfig applies defaults to the config entry.
|
||||
func applyDefaultsToConfig(config *codersdk.ExternalAuthConfig) {
|
||||
defaults := defaults[codersdk.EnhancedExternalAuthProvider(config.Type)]
|
||||
if config.AuthURL == "" {
|
||||
config.AuthURL = defaults.AuthURL
|
||||
}
|
||||
if config.TokenURL == "" {
|
||||
config.TokenURL = defaults.TokenURL
|
||||
}
|
||||
if config.ValidateURL == "" {
|
||||
config.ValidateURL = defaults.ValidateURL
|
||||
}
|
||||
if config.AppInstallURL == "" {
|
||||
config.AppInstallURL = defaults.AppInstallURL
|
||||
}
|
||||
if config.AppInstallationsURL == "" {
|
||||
config.AppInstallationsURL = defaults.AppInstallationsURL
|
||||
}
|
||||
if config.Regex == "" {
|
||||
config.Regex = defaults.Regex
|
||||
}
|
||||
if config.Scopes == nil || len(config.Scopes) == 0 {
|
||||
config.Scopes = defaults.Scopes
|
||||
}
|
||||
if config.DeviceCodeURL == "" {
|
||||
config.DeviceCodeURL = defaults.DeviceCodeURL
|
||||
}
|
||||
if config.DisplayName == "" {
|
||||
config.DisplayName = defaults.DisplayName
|
||||
}
|
||||
if config.DisplayIcon == "" {
|
||||
config.DisplayIcon = defaults.DisplayIcon
|
||||
}
|
||||
|
||||
// Apply defaults if it's still empty...
|
||||
if config.ID == "" {
|
||||
config.ID = config.Type
|
||||
}
|
||||
if config.DisplayName == "" {
|
||||
config.DisplayName = config.Type
|
||||
}
|
||||
if config.DisplayIcon == "" {
|
||||
// This is a key emoji.
|
||||
config.DisplayIcon = "/emojis/1f511.png"
|
||||
}
|
||||
}
|
||||
|
||||
var defaults = map[codersdk.EnhancedExternalAuthProvider]codersdk.ExternalAuthConfig{
|
||||
codersdk.EnhancedExternalAuthProviderAzureDevops: {
|
||||
AuthURL: "https://app.vssps.visualstudio.com/oauth2/authorize",
|
||||
TokenURL: "https://app.vssps.visualstudio.com/oauth2/token",
|
||||
DisplayName: "Azure DevOps",
|
||||
DisplayIcon: "/icon/azure-devops.svg",
|
||||
Regex: `^(https?://)?dev\.azure\.com(/.*)?$`,
|
||||
Scopes: []string{"vso.code_write"},
|
||||
},
|
||||
codersdk.EnhancedExternalAuthProviderBitBucket: {
|
||||
AuthURL: "https://bitbucket.org/site/oauth2/authorize",
|
||||
TokenURL: "https://bitbucket.org/site/oauth2/access_token",
|
||||
ValidateURL: "https://api.bitbucket.org/2.0/user",
|
||||
DisplayName: "BitBucket",
|
||||
DisplayIcon: "/icon/bitbucket.svg",
|
||||
Regex: `^(https?://)?bitbucket\.org(/.*)?$`,
|
||||
Scopes: []string{"account", "repository:write"},
|
||||
},
|
||||
codersdk.EnhancedExternalAuthProviderGitLab: {
|
||||
AuthURL: "https://gitlab.com/oauth/authorize",
|
||||
TokenURL: "https://gitlab.com/oauth/token",
|
||||
ValidateURL: "https://gitlab.com/oauth/token/info",
|
||||
DisplayName: "GitLab",
|
||||
DisplayIcon: "/icon/gitlab.svg",
|
||||
Regex: `^(https?://)?gitlab\.com(/.*)?$`,
|
||||
Scopes: []string{"write_repository"},
|
||||
},
|
||||
codersdk.EnhancedExternalAuthProviderGitHub: {
|
||||
AuthURL: xgithub.Endpoint.AuthURL,
|
||||
TokenURL: xgithub.Endpoint.TokenURL,
|
||||
ValidateURL: "https://api.github.com/user",
|
||||
DisplayName: "GitHub",
|
||||
DisplayIcon: "/icon/github.svg",
|
||||
Regex: `^(https?://)?github\.com(/.*)?$`,
|
||||
// "workflow" is required for managing GitHub Actions in a repository.
|
||||
Scopes: []string{"repo", "workflow"},
|
||||
DeviceCodeURL: "https://github.com/login/device/code",
|
||||
AppInstallationsURL: "https://api.github.com/user/installations",
|
||||
},
|
||||
}
|
||||
|
||||
// jwtConfig is a new OAuth2 config that uses a custom
|
||||
// assertion method that works with Azure Devops. See:
|
||||
// https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/oauth?view=azure-devops
|
||||
type jwtConfig struct {
|
||||
*oauth2.Config
|
||||
}
|
||||
|
||||
func (c *jwtConfig) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
|
||||
return c.Config.AuthCodeURL(state, append(opts, oauth2.SetAuthURLParam("response_type", "Assertion"))...)
|
||||
}
|
||||
|
||||
func (c *jwtConfig) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
|
||||
v := url.Values{
|
||||
"client_assertion_type": {},
|
||||
"client_assertion": {c.ClientSecret},
|
||||
"assertion": {code},
|
||||
"grant_type": {},
|
||||
}
|
||||
if c.RedirectURL != "" {
|
||||
v.Set("redirect_uri", c.RedirectURL)
|
||||
}
|
||||
return c.Config.Exchange(ctx, code,
|
||||
append(opts,
|
||||
oauth2.SetAuthURLParam("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"),
|
||||
oauth2.SetAuthURLParam("client_assertion", c.ClientSecret),
|
||||
oauth2.SetAuthURLParam("assertion", code),
|
||||
oauth2.SetAuthURLParam("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
|
||||
oauth2.SetAuthURLParam("code", ""),
|
||||
)...,
|
||||
)
|
||||
}
|
@ -176,7 +176,7 @@ func TestRefreshToken(t *testing.T) {
|
||||
}),
|
||||
},
|
||||
GitConfigOpt: func(cfg *externalauth.Config) {
|
||||
cfg.Type = codersdk.ExternalAuthProviderGitHub
|
||||
cfg.Type = codersdk.EnhancedExternalAuthProviderGitHub.String()
|
||||
},
|
||||
})
|
||||
|
||||
@ -206,7 +206,7 @@ func TestRefreshToken(t *testing.T) {
|
||||
}),
|
||||
},
|
||||
GitConfigOpt: func(cfg *externalauth.Config) {
|
||||
cfg.Type = codersdk.ExternalAuthProviderGitHub
|
||||
cfg.Type = codersdk.EnhancedExternalAuthProviderGitHub.String()
|
||||
},
|
||||
})
|
||||
|
||||
@ -237,7 +237,7 @@ func TestRefreshToken(t *testing.T) {
|
||||
}),
|
||||
},
|
||||
GitConfigOpt: func(cfg *externalauth.Config) {
|
||||
cfg.Type = codersdk.ExternalAuthProviderGitHub
|
||||
cfg.Type = codersdk.EnhancedExternalAuthProviderGitHub.String()
|
||||
},
|
||||
DB: db,
|
||||
})
|
||||
@ -266,42 +266,38 @@ func TestConvertYAML(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, tc := range []struct {
|
||||
Name string
|
||||
Input []codersdk.GitAuthConfig
|
||||
Input []codersdk.ExternalAuthConfig
|
||||
Output []*externalauth.Config
|
||||
Error string
|
||||
}{{
|
||||
Name: "InvalidType",
|
||||
Input: []codersdk.GitAuthConfig{{
|
||||
Type: "moo",
|
||||
}},
|
||||
Error: "unknown git provider type",
|
||||
}, {
|
||||
Name: "InvalidID",
|
||||
Input: []codersdk.GitAuthConfig{{
|
||||
Type: string(codersdk.ExternalAuthProviderGitHub),
|
||||
Input: []codersdk.ExternalAuthConfig{{
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitHub),
|
||||
ID: "$hi$",
|
||||
}},
|
||||
Error: "doesn't have a valid id",
|
||||
}, {
|
||||
Name: "NoClientID",
|
||||
Input: []codersdk.GitAuthConfig{{
|
||||
Type: string(codersdk.ExternalAuthProviderGitHub),
|
||||
Input: []codersdk.ExternalAuthConfig{{
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitHub),
|
||||
}},
|
||||
Error: "client_id must be provided",
|
||||
}, {
|
||||
Name: "DuplicateType",
|
||||
Input: []codersdk.GitAuthConfig{{
|
||||
Type: string(codersdk.ExternalAuthProviderGitHub),
|
||||
Input: []codersdk.ExternalAuthConfig{{
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitHub),
|
||||
ClientID: "example",
|
||||
ClientSecret: "example",
|
||||
}, {
|
||||
Type: string(codersdk.ExternalAuthProviderGitHub),
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitHub),
|
||||
ClientID: "example-2",
|
||||
ClientSecret: "example-2",
|
||||
}},
|
||||
Error: "multiple github external auth providers provided",
|
||||
}, {
|
||||
Name: "InvalidRegex",
|
||||
Input: []codersdk.GitAuthConfig{{
|
||||
Type: string(codersdk.ExternalAuthProviderGitHub),
|
||||
Input: []codersdk.ExternalAuthConfig{{
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitHub),
|
||||
ClientID: "example",
|
||||
ClientSecret: "example",
|
||||
Regex: `\K`,
|
||||
@ -309,8 +305,8 @@ func TestConvertYAML(t *testing.T) {
|
||||
Error: "compile regex for external auth provider",
|
||||
}, {
|
||||
Name: "NoDeviceURL",
|
||||
Input: []codersdk.GitAuthConfig{{
|
||||
Type: string(codersdk.ExternalAuthProviderGitLab),
|
||||
Input: []codersdk.ExternalAuthConfig{{
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
|
||||
ClientID: "example",
|
||||
ClientSecret: "example",
|
||||
DeviceFlow: true,
|
||||
@ -332,8 +328,8 @@ func TestConvertYAML(t *testing.T) {
|
||||
|
||||
t.Run("CustomScopesAndEndpoint", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
config, err := externalauth.ConvertConfig([]codersdk.GitAuthConfig{{
|
||||
Type: string(codersdk.ExternalAuthProviderGitLab),
|
||||
config, err := externalauth.ConvertConfig([]codersdk.ExternalAuthConfig{{
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
|
||||
ClientID: "id",
|
||||
ClientSecret: "secret",
|
||||
AuthURL: "https://auth.com",
|
||||
@ -341,7 +337,7 @@ func TestConvertYAML(t *testing.T) {
|
||||
Scopes: []string{"read"},
|
||||
}}, &url.URL{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "https://auth.com?client_id=id&redirect_uri=%2Fexternalauth%2Fgitlab%2Fcallback&response_type=code&scope=read", config[0].AuthCodeURL(""))
|
||||
require.Equal(t, "https://auth.com?client_id=id&redirect_uri=%2Fexternal-auth%2Fgitlab%2Fcallback&response_type=code&scope=read", config[0].AuthCodeURL(""))
|
||||
})
|
||||
}
|
||||
|
@ -1,212 +0,0 @@
|
||||
package externalauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/github"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// endpoint contains default SaaS URLs for each Git provider.
|
||||
var endpoint = map[codersdk.ExternalAuthProvider]oauth2.Endpoint{
|
||||
codersdk.ExternalAuthProviderAzureDevops: {
|
||||
AuthURL: "https://app.vssps.visualstudio.com/oauth2/authorize",
|
||||
TokenURL: "https://app.vssps.visualstudio.com/oauth2/token",
|
||||
},
|
||||
codersdk.ExternalAuthProviderBitBucket: {
|
||||
AuthURL: "https://bitbucket.org/site/oauth2/authorize",
|
||||
TokenURL: "https://bitbucket.org/site/oauth2/access_token",
|
||||
},
|
||||
codersdk.ExternalAuthProviderGitLab: {
|
||||
AuthURL: "https://gitlab.com/oauth/authorize",
|
||||
TokenURL: "https://gitlab.com/oauth/token",
|
||||
},
|
||||
codersdk.ExternalAuthProviderGitHub: github.Endpoint,
|
||||
}
|
||||
|
||||
// validateURL contains defaults for each provider.
|
||||
var validateURL = map[codersdk.ExternalAuthProvider]string{
|
||||
codersdk.ExternalAuthProviderGitHub: "https://api.github.com/user",
|
||||
codersdk.ExternalAuthProviderGitLab: "https://gitlab.com/oauth/token/info",
|
||||
codersdk.ExternalAuthProviderBitBucket: "https://api.bitbucket.org/2.0/user",
|
||||
}
|
||||
|
||||
var deviceAuthURL = map[codersdk.ExternalAuthProvider]string{
|
||||
codersdk.ExternalAuthProviderGitHub: "https://github.com/login/device/code",
|
||||
}
|
||||
|
||||
var appInstallationsURL = map[codersdk.ExternalAuthProvider]string{
|
||||
codersdk.ExternalAuthProviderGitHub: "https://api.github.com/user/installations",
|
||||
}
|
||||
|
||||
// scope contains defaults for each Git provider.
|
||||
var scope = map[codersdk.ExternalAuthProvider][]string{
|
||||
codersdk.ExternalAuthProviderAzureDevops: {"vso.code_write"},
|
||||
codersdk.ExternalAuthProviderBitBucket: {"account", "repository:write"},
|
||||
codersdk.ExternalAuthProviderGitLab: {"write_repository"},
|
||||
// "workflow" is required for managing GitHub Actions in a repository.
|
||||
codersdk.ExternalAuthProviderGitHub: {"repo", "workflow"},
|
||||
}
|
||||
|
||||
// regex provides defaults for each Git provider to match their SaaS host URL.
|
||||
// This is configurable by each provider.
|
||||
var regex = map[codersdk.ExternalAuthProvider]*regexp.Regexp{
|
||||
codersdk.ExternalAuthProviderAzureDevops: regexp.MustCompile(`^(https?://)?dev\.azure\.com(/.*)?$`),
|
||||
codersdk.ExternalAuthProviderBitBucket: regexp.MustCompile(`^(https?://)?bitbucket\.org(/.*)?$`),
|
||||
codersdk.ExternalAuthProviderGitLab: regexp.MustCompile(`^(https?://)?gitlab\.com(/.*)?$`),
|
||||
codersdk.ExternalAuthProviderGitHub: regexp.MustCompile(`^(https?://)?github\.com(/.*)?$`),
|
||||
}
|
||||
|
||||
// jwtConfig is a new OAuth2 config that uses a custom
|
||||
// assertion method that works with Azure Devops. See:
|
||||
// https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/oauth?view=azure-devops
|
||||
type jwtConfig struct {
|
||||
*oauth2.Config
|
||||
}
|
||||
|
||||
func (c *jwtConfig) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
|
||||
return c.Config.AuthCodeURL(state, append(opts, oauth2.SetAuthURLParam("response_type", "Assertion"))...)
|
||||
}
|
||||
|
||||
func (c *jwtConfig) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
|
||||
v := url.Values{
|
||||
"client_assertion_type": {},
|
||||
"client_assertion": {c.ClientSecret},
|
||||
"assertion": {code},
|
||||
"grant_type": {},
|
||||
}
|
||||
if c.RedirectURL != "" {
|
||||
v.Set("redirect_uri", c.RedirectURL)
|
||||
}
|
||||
return c.Config.Exchange(ctx, code,
|
||||
append(opts,
|
||||
oauth2.SetAuthURLParam("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"),
|
||||
oauth2.SetAuthURLParam("client_assertion", c.ClientSecret),
|
||||
oauth2.SetAuthURLParam("assertion", code),
|
||||
oauth2.SetAuthURLParam("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
|
||||
oauth2.SetAuthURLParam("code", ""),
|
||||
)...,
|
||||
)
|
||||
}
|
||||
|
||||
type DeviceAuth struct {
|
||||
ClientID string
|
||||
TokenURL string
|
||||
Scopes []string
|
||||
CodeURL string
|
||||
}
|
||||
|
||||
// AuthorizeDevice begins the device authorization flow.
|
||||
// See: https://tools.ietf.org/html/rfc8628#section-3.1
|
||||
func (c *DeviceAuth) AuthorizeDevice(ctx context.Context) (*codersdk.ExternalAuthDevice, error) {
|
||||
if c.CodeURL == "" {
|
||||
return nil, xerrors.New("oauth2: device code URL not set")
|
||||
}
|
||||
codeURL, err := c.formatDeviceCodeURL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, codeURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var r struct {
|
||||
codersdk.ExternalAuthDevice
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
err = json.NewDecoder(resp.Body).Decode(&r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.ErrorDescription != "" {
|
||||
return nil, xerrors.New(r.ErrorDescription)
|
||||
}
|
||||
return &r.ExternalAuthDevice, nil
|
||||
}
|
||||
|
||||
type ExchangeDeviceCodeResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
|
||||
// ExchangeDeviceCode exchanges a device code for an access token.
|
||||
// The boolean returned indicates whether the device code is still pending
|
||||
// and the caller should try again.
|
||||
func (c *DeviceAuth) ExchangeDeviceCode(ctx context.Context, deviceCode string) (*oauth2.Token, error) {
|
||||
if c.TokenURL == "" {
|
||||
return nil, xerrors.New("oauth2: token URL not set")
|
||||
}
|
||||
tokenURL, err := c.formatDeviceTokenURL(deviceCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, codersdk.ReadBodyAsError(resp)
|
||||
}
|
||||
var body ExchangeDeviceCodeResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if body.Error != "" {
|
||||
return nil, xerrors.New(body.Error)
|
||||
}
|
||||
return &oauth2.Token{
|
||||
AccessToken: body.AccessToken,
|
||||
RefreshToken: body.RefreshToken,
|
||||
Expiry: dbtime.Now().Add(time.Duration(body.ExpiresIn) * time.Second),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *DeviceAuth) formatDeviceTokenURL(deviceCode string) (string, error) {
|
||||
tok, err := url.Parse(c.TokenURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tok.RawQuery = url.Values{
|
||||
"client_id": {c.ClientID},
|
||||
"device_code": {deviceCode},
|
||||
"grant_type": {"urn:ietf:params:oauth:grant-type:device_code"},
|
||||
}.Encode()
|
||||
return tok.String(), nil
|
||||
}
|
||||
|
||||
func (c *DeviceAuth) formatDeviceCodeURL() (string, error) {
|
||||
cod, err := url.Parse(c.CodeURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
cod.RawQuery = url.Values{
|
||||
"client_id": {c.ClientID},
|
||||
"scope": c.Scopes,
|
||||
}.Encode()
|
||||
return cod.String(), nil
|
||||
}
|
@ -34,7 +34,7 @@ func TestExternalAuthByID(t *testing.T) {
|
||||
ExternalAuthConfigs: []*externalauth.Config{{
|
||||
ID: "test",
|
||||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
}},
|
||||
})
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
@ -51,7 +51,7 @@ func TestExternalAuthByID(t *testing.T) {
|
||||
ID: "test",
|
||||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
// AzureDevops doesn't have a user endpoint!
|
||||
Type: codersdk.ExternalAuthProviderAzureDevops,
|
||||
Type: codersdk.EnhancedExternalAuthProviderAzureDevops.String(),
|
||||
}},
|
||||
})
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
@ -75,7 +75,7 @@ func TestExternalAuthByID(t *testing.T) {
|
||||
ID: "test",
|
||||
ValidateURL: validateSrv.URL,
|
||||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
}},
|
||||
})
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
@ -116,7 +116,7 @@ func TestExternalAuthByID(t *testing.T) {
|
||||
ValidateURL: srv.URL + "/user",
|
||||
AppInstallationsURL: srv.URL + "/installs",
|
||||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
}},
|
||||
})
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
@ -249,7 +249,7 @@ func TestGitAuthCallback(t *testing.T) {
|
||||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
ID: "github",
|
||||
Regex: regexp.MustCompile(`github\.com`),
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
}},
|
||||
})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
@ -268,7 +268,7 @@ func TestGitAuthCallback(t *testing.T) {
|
||||
agentClient.SetSessionToken(authToken)
|
||||
token, err := agentClient.GitAuth(context.Background(), "github.com/asd/asd", false)
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.HasSuffix(token.URL, fmt.Sprintf("/externalauth/%s", "github")))
|
||||
require.True(t, strings.HasSuffix(token.URL, fmt.Sprintf("/external-auth/%s", "github")), token.URL)
|
||||
})
|
||||
t.Run("UnauthorizedCallback", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@ -278,7 +278,7 @@ func TestGitAuthCallback(t *testing.T) {
|
||||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
ID: "github",
|
||||
Regex: regexp.MustCompile(`github\.com`),
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
}},
|
||||
})
|
||||
resp := coderdtest.RequestExternalAuthCallback(t, "github", client)
|
||||
@ -292,7 +292,7 @@ func TestGitAuthCallback(t *testing.T) {
|
||||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
ID: "github",
|
||||
Regex: regexp.MustCompile(`github\.com`),
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
}},
|
||||
})
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
@ -300,7 +300,7 @@ func TestGitAuthCallback(t *testing.T) {
|
||||
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
location, err := resp.Location()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "/externalauth/github", location.Path)
|
||||
require.Equal(t, "/external-auth/github", location.Path)
|
||||
|
||||
// Callback again to simulate updating the token.
|
||||
resp = coderdtest.RequestExternalAuthCallback(t, "github", client)
|
||||
@ -319,7 +319,7 @@ func TestGitAuthCallback(t *testing.T) {
|
||||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
ID: "github",
|
||||
Regex: regexp.MustCompile(`github\.com`),
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
}},
|
||||
})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
@ -376,7 +376,7 @@ func TestGitAuthCallback(t *testing.T) {
|
||||
},
|
||||
ID: "github",
|
||||
Regex: regexp.MustCompile(`github\.com`),
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
NoRefresh: true,
|
||||
}},
|
||||
})
|
||||
@ -420,7 +420,7 @@ func TestGitAuthCallback(t *testing.T) {
|
||||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
ID: "github",
|
||||
Regex: regexp.MustCompile(`github\.com`),
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
}},
|
||||
})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
|
@ -65,9 +65,9 @@ func Test_RoutePatterns(t *testing.T) {
|
||||
"/api/**",
|
||||
"/@*/*/apps/**",
|
||||
"/%40*/*/apps/**",
|
||||
"/externalauth/*/callback",
|
||||
"/external-auth/*/callback",
|
||||
},
|
||||
output: "^(/api/?|/api/.+/?|/@[^/]+/[^/]+/apps/.+/?|/%40[^/]+/[^/]+/apps/.+/?|/externalauth/[^/]+/callback/?)$",
|
||||
output: "^(/api/?|/api/.+/?|/@[^/]+/[^/]+/apps/.+/?|/%40[^/]+/[^/]+/apps/.+/?|/external-auth/[^/]+/callback/?)$",
|
||||
},
|
||||
{
|
||||
name: "Slash",
|
||||
|
@ -280,7 +280,7 @@ func (api *API) templateVersionRichParameters(rw http.ResponseWriter, r *http.Re
|
||||
// @Tags Templates
|
||||
// @Param templateversion path string true "Template version ID" format(uuid)
|
||||
// @Success 200 {array} codersdk.TemplateVersionExternalAuth
|
||||
// @Router /templateversions/{templateversion}/externalauth [get]
|
||||
// @Router /templateversions/{templateversion}/external-auth [get]
|
||||
func (api *API) templateVersionExternalAuth(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var (
|
||||
@ -307,7 +307,7 @@ func (api *API) templateVersionExternalAuth(rw http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
// This is the URL that will redirect the user with a state token.
|
||||
redirectURL, err := api.AccessURL.Parse(fmt.Sprintf("/externalauth/%s", config.ID))
|
||||
redirectURL, err := api.AccessURL.Parse(fmt.Sprintf("/external-auth/%s", config.ID))
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to parse access URL.",
|
||||
@ -320,6 +320,8 @@ func (api *API) templateVersionExternalAuth(rw http.ResponseWriter, r *http.Requ
|
||||
ID: config.ID,
|
||||
Type: config.Type,
|
||||
AuthenticateURL: redirectURL.String(),
|
||||
DisplayName: config.DisplayName,
|
||||
DisplayIcon: config.DisplayIcon,
|
||||
}
|
||||
|
||||
authLink, err := api.Database.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{
|
||||
|
@ -342,7 +342,7 @@ func TestTemplateVersionsExternalAuth(t *testing.T) {
|
||||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
ID: "github",
|
||||
Regex: regexp.MustCompile(`github\.com`),
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
}},
|
||||
})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
|
@ -26,7 +26,7 @@ func Middleware(tracerProvider trace.TracerProvider) func(http.Handler) http.Han
|
||||
"/api/**",
|
||||
"/@*/*/apps/**",
|
||||
"/%40*/*/apps/**",
|
||||
"/externalauth/*/callback",
|
||||
"/external-auth/*/callback",
|
||||
}.MustCompile()
|
||||
|
||||
var tracer trace.Tracer
|
||||
|
@ -59,7 +59,7 @@ func Test_Middleware(t *testing.T) {
|
||||
{"/%40hi/hi/apps/hi", true},
|
||||
{"/%40hi/hi/apps/hi/hi", true},
|
||||
{"/%40hi/hi/apps/hi/hi", true},
|
||||
{"/externalauth/hi/callback", true},
|
||||
{"/external-auth/hi/callback", true},
|
||||
|
||||
// Other routes that should not be collected.
|
||||
{"/index.html", false},
|
||||
|
@ -2201,6 +2201,13 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request)
|
||||
})
|
||||
return
|
||||
}
|
||||
enhancedType := codersdk.EnhancedExternalAuthProvider(externalAuthConfig.Type)
|
||||
if !enhancedType.Git() {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "External auth provider does not support git.",
|
||||
})
|
||||
return
|
||||
}
|
||||
workspaceAgent := httpmw.WorkspaceAgent(r)
|
||||
// We must get the workspace to get the owner ID!
|
||||
resource, err := api.Database.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID)
|
||||
@ -2272,13 +2279,13 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request)
|
||||
if !valid {
|
||||
continue
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusOK, formatGitAuthAccessToken(externalAuthConfig.Type, externalAuthLink.OAuthAccessToken))
|
||||
httpapi.Write(ctx, rw, http.StatusOK, formatGitAuthAccessToken(enhancedType, externalAuthLink.OAuthAccessToken))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// This is the URL that will redirect the user with a state token.
|
||||
redirectURL, err := api.AccessURL.Parse(fmt.Sprintf("/externalauth/%s", externalAuthConfig.ID))
|
||||
redirectURL, err := api.AccessURL.Parse(fmt.Sprintf("/external-auth/%s", externalAuthConfig.ID))
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to parse access URL.",
|
||||
@ -2320,20 +2327,20 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request)
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusOK, formatGitAuthAccessToken(externalAuthConfig.Type, externalAuthLink.OAuthAccessToken))
|
||||
httpapi.Write(ctx, rw, http.StatusOK, formatGitAuthAccessToken(enhancedType, externalAuthLink.OAuthAccessToken))
|
||||
}
|
||||
|
||||
// Provider types have different username/password formats.
|
||||
func formatGitAuthAccessToken(typ codersdk.ExternalAuthProvider, token string) agentsdk.GitAuthResponse {
|
||||
func formatGitAuthAccessToken(typ codersdk.EnhancedExternalAuthProvider, token string) agentsdk.GitAuthResponse {
|
||||
var resp agentsdk.GitAuthResponse
|
||||
switch typ {
|
||||
case codersdk.ExternalAuthProviderGitLab:
|
||||
case codersdk.EnhancedExternalAuthProviderGitLab:
|
||||
// https://stackoverflow.com/questions/25409700/using-gitlab-token-to-clone-without-authentication
|
||||
resp = agentsdk.GitAuthResponse{
|
||||
Username: "oauth2",
|
||||
Password: token,
|
||||
}
|
||||
case codersdk.ExternalAuthProviderBitBucket:
|
||||
case codersdk.EnhancedExternalAuthProviderBitBucket:
|
||||
// https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/#Cloning-a-repository-with-an-access-token
|
||||
resp = agentsdk.GitAuthResponse{
|
||||
Username: "x-token-auth",
|
||||
|
Reference in New Issue
Block a user