mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add redirect URL override for external auth (#28082)
This commit is contained in:
Generated
+5
-1
@@ -20739,6 +20739,10 @@ const docTemplate = `{
|
||||
"no_refresh": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"redirect_url": {
|
||||
"description": "RedirectURL is optional, defaulting to 'ACCESS_URL'. Only useful in niche\nsituations where the OAuth callback domain is different from the ACCESS_URL\ndomain. The path component is ignored.",
|
||||
"type": "string"
|
||||
},
|
||||
"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"
|
||||
@@ -22434,7 +22438,7 @@ const docTemplate = `{
|
||||
}
|
||||
},
|
||||
"redirect_url": {
|
||||
"description": "RedirectURL is optional, defaulting to 'ACCESS_URL'. Only useful in niche\nsituations where the OIDC callback domain is different from the ACCESS_URL\ndomain.",
|
||||
"description": "RedirectURL is optional, defaulting to 'ACCESS_URL'. Only useful in niche\nsituations where the OIDC callback domain is different from the ACCESS_URL\ndomain. The path component is ignored.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/serpent.URL"
|
||||
|
||||
Generated
+5
-1
@@ -18861,6 +18861,10 @@
|
||||
"no_refresh": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"redirect_url": {
|
||||
"description": "RedirectURL is optional, defaulting to 'ACCESS_URL'. Only useful in niche\nsituations where the OAuth callback domain is different from the ACCESS_URL\ndomain. The path component is ignored.",
|
||||
"type": "string"
|
||||
},
|
||||
"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"
|
||||
@@ -20488,7 +20492,7 @@
|
||||
}
|
||||
},
|
||||
"redirect_url": {
|
||||
"description": "RedirectURL is optional, defaulting to 'ACCESS_URL'. Only useful in niche\nsituations where the OIDC callback domain is different from the ACCESS_URL\ndomain.",
|
||||
"description": "RedirectURL is optional, defaulting to 'ACCESS_URL'. Only useful in niche\nsituations where the OIDC callback domain is different from the ACCESS_URL\ndomain. The path component is ignored.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/serpent.URL"
|
||||
|
||||
@@ -912,7 +912,7 @@ func (c *DeviceAuth) formatDeviceCodeURL() (string, error) {
|
||||
|
||||
// ConvertConfig converts the SDK configuration entry format
|
||||
// to the parsed and ready-to-consume in coderd provider type.
|
||||
func ConvertConfig(logger slog.Logger, instrument *promoauth.Factory, entries []codersdk.ExternalAuthConfig, accessURL *url.URL) ([]*Config, error) {
|
||||
func ConvertConfig(ctx context.Context, logger slog.Logger, instrument *promoauth.Factory, entries []codersdk.ExternalAuthConfig, accessURL *url.URL) ([]*Config, error) {
|
||||
ids := map[string]struct{}{}
|
||||
configs := []*Config{}
|
||||
for _, entry := range entries {
|
||||
@@ -921,6 +921,8 @@ func ConvertConfig(logger slog.Logger, instrument *promoauth.Factory, entries []
|
||||
// apply their client secret and ID, and have the UI appear nicely.
|
||||
applyDefaultsToConfig(&entry)
|
||||
|
||||
logger := logger.Named("externalauth").With(slog.F("provider_id", entry.ID), slog.F("provider_type", entry.Type))
|
||||
|
||||
valid := codersdk.NameValid(entry.ID)
|
||||
if valid != nil {
|
||||
return nil, xerrors.Errorf("external auth provider %q doesn't have a valid id: %w", entry.ID, valid)
|
||||
@@ -938,9 +940,18 @@ func ConvertConfig(logger slog.Logger, instrument *promoauth.Factory, entries []
|
||||
}
|
||||
ids[entry.ID] = struct{}{}
|
||||
|
||||
authRedirect, err := accessURL.Parse(fmt.Sprintf("/external-auth/%s/callback", entry.ID))
|
||||
baseRedirectURL := accessURL
|
||||
if entry.RedirectURL != "" {
|
||||
var err error
|
||||
baseRedirectURL, err = url.Parse(entry.RedirectURL)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("parse redirect url override for external auth provider %q: %w", entry.ID, err)
|
||||
}
|
||||
logger.Warn(ctx, "custom redirect URL used instead of 'access_url', ensure this matches the value configured in your provider")
|
||||
}
|
||||
authRedirect, err := baseRedirectURL.Parse(fmt.Sprintf("/external-auth/%s/callback", entry.ID))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("parse external auth callback url: %w", err)
|
||||
return nil, xerrors.Errorf("parse callback url for external auth provider %q: %w", entry.ID, err)
|
||||
}
|
||||
|
||||
var regex *regexp.Regexp
|
||||
@@ -996,7 +1007,7 @@ func ConvertConfig(logger slog.Logger, instrument *promoauth.Factory, entries []
|
||||
|
||||
cfg := &Config{
|
||||
InstrumentedOAuth2Config: instrumented,
|
||||
Logger: logger.Named("externalauth").With(slog.F("provider_id", entry.ID), slog.F("provider_type", entry.Type)),
|
||||
Logger: logger,
|
||||
ID: entry.ID,
|
||||
ClientID: entry.ClientID,
|
||||
ClientSecret: entry.ClientSecret,
|
||||
@@ -1092,6 +1103,9 @@ func copyDefaultSettings(config *codersdk.ExternalAuthConfig, defaults codersdk.
|
||||
if config.ValidateURL == "" {
|
||||
config.ValidateURL = defaults.ValidateURL
|
||||
}
|
||||
if config.RedirectURL == "" {
|
||||
config.RedirectURL = defaults.RedirectURL
|
||||
}
|
||||
if config.RevokeURL == "" {
|
||||
config.RevokeURL = defaults.RevokeURL
|
||||
}
|
||||
|
||||
@@ -1076,7 +1076,7 @@ func TestRefreshTokenWithScopes(t *testing.T) {
|
||||
newConfig := func(t *testing.T, scopes []string) *externalauth.Config {
|
||||
t.Helper()
|
||||
instrument := promoauth.NewFactory(prometheus.NewRegistry())
|
||||
configs, err := externalauth.ConvertConfig(testutil.Logger(t), instrument, []codersdk.ExternalAuthConfig{{
|
||||
configs, err := externalauth.ConvertConfig(context.Background(), testutil.Logger(t), instrument, []codersdk.ExternalAuthConfig{{
|
||||
ID: "test",
|
||||
Type: codersdk.EnhancedExternalAuthProviderAzureDevopsEntra.String(),
|
||||
ClientID: "id",
|
||||
@@ -1203,7 +1203,7 @@ func TestValidateToken(t *testing.T) {
|
||||
logs := &bytes.Buffer{}
|
||||
logger := slog.Make(slogjson.Sink(logs)).Leveled(slog.LevelDebug)
|
||||
// ConvertConfig wires the named logger as production does.
|
||||
configs, err := externalauth.ConvertConfig(logger, f, []codersdk.ExternalAuthConfig{{
|
||||
configs, err := externalauth.ConvertConfig(context.Background(), logger, f, []codersdk.ExternalAuthConfig{{
|
||||
ID: providerName,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
ClientID: "id",
|
||||
@@ -1608,7 +1608,7 @@ func TestExchangeWithClientSecret(t *testing.T) {
|
||||
instrument := promoauth.NewFactory(prometheus.NewRegistry())
|
||||
// This ensures a provider that requires the custom
|
||||
// client secret exchange works.
|
||||
configs, err := externalauth.ConvertConfig(testutil.Logger(t), instrument, []codersdk.ExternalAuthConfig{{
|
||||
configs, err := externalauth.ConvertConfig(context.Background(), testutil.Logger(t), instrument, []codersdk.ExternalAuthConfig{{
|
||||
// JFrog just happens to require this custom type.
|
||||
|
||||
Type: codersdk.EnhancedExternalAuthProviderJFrog.String(),
|
||||
@@ -1740,7 +1740,7 @@ func TestConvertYAML(t *testing.T) {
|
||||
}} {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
output, err := externalauth.ConvertConfig(testutil.Logger(t), instrument, tc.Input, &url.URL{})
|
||||
output, err := externalauth.ConvertConfig(context.Background(), testutil.Logger(t), instrument, tc.Input, &url.URL{})
|
||||
if tc.Error != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.Error)
|
||||
@@ -1752,21 +1752,22 @@ func TestConvertYAML(t *testing.T) {
|
||||
|
||||
t.Run("CustomScopesAndEndpoint", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
config, err := externalauth.ConvertConfig(testutil.Logger(t), instrument, []codersdk.ExternalAuthConfig{{
|
||||
config, err := externalauth.ConvertConfig(context.Background(), testutil.Logger(t), instrument, []codersdk.ExternalAuthConfig{{
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
|
||||
ClientID: "id",
|
||||
ClientSecret: "secret",
|
||||
AuthURL: "https://auth.com",
|
||||
TokenURL: "https://token.com",
|
||||
RedirectURL: "https://redirect.com",
|
||||
Scopes: []string{"read"},
|
||||
}}, &url.URL{})
|
||||
}}, &url.URL{Scheme: "https", Host: "default.com"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "https://auth.com?client_id=id&redirect_uri=%2Fexternal-auth%2Fgitlab%2Fcallback&response_type=code&scope=read", config[0].AuthCodeURL(""))
|
||||
require.Equal(t, "https://auth.com?client_id=id&redirect_uri=https%3A%2F%2Fredirect.com%2Fexternal-auth%2Fgitlab%2Fcallback&response_type=code&scope=read", config[0].AuthCodeURL(""))
|
||||
})
|
||||
|
||||
t.Run("RevokeTimeoutSet", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
configs, err := externalauth.ConvertConfig(testutil.Logger(t), instrument, []codersdk.ExternalAuthConfig{{
|
||||
configs, err := externalauth.ConvertConfig(context.Background(), testutil.Logger(t), instrument, []codersdk.ExternalAuthConfig{{
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
|
||||
ClientID: "id",
|
||||
ClientSecret: "secret",
|
||||
@@ -1777,7 +1778,7 @@ func TestConvertYAML(t *testing.T) {
|
||||
|
||||
t.Run("SelfHostedGitLabAPIBaseURL", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
configs, err := externalauth.ConvertConfig(testutil.Logger(t), instrument, []codersdk.ExternalAuthConfig{{
|
||||
configs, err := externalauth.ConvertConfig(context.Background(), testutil.Logger(t), instrument, []codersdk.ExternalAuthConfig{{
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
|
||||
ClientID: "id",
|
||||
ClientSecret: "secret",
|
||||
@@ -1956,6 +1957,7 @@ func TestApplyDefaultsToConfig_CaseInsensitive(t *testing.T) {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
configs, err := externalauth.ConvertConfig(
|
||||
context.Background(),
|
||||
testutil.Logger(t),
|
||||
instrument,
|
||||
[]codersdk.ExternalAuthConfig{{
|
||||
|
||||
Reference in New Issue
Block a user