mirror of https://github.com/coder/coder.git
feat: Allow hiding password auth, changing OpenID Connect text and OpenID Connect icon (#5101)
* Allow hiding password entry, changing OpenID Connect text and OpenID Connect icon * Docs * Cleaning * Fix Prettier and Go test and TS compile error * Fix LoginPage test * Prettier * Fix storybook * Add query param to un-hide password auth * Cleaning * Hide password by default when OIDC enabled * Ran prettier, updated goldenfiles and ran "make gen" * Fixed and added LoginPage test * Ran prettier * PR Feedback and split up SignInForm.tsx * Updated golden files * Fix auto-genned-files * make gen -B * Revert provisioner files? * Fix lint error --------- Co-authored-by: Kyle Carberry <kyle@coder.com>
This commit is contained in:
parent
480f3b6e43
commit
69fce0488e
|
@ -254,6 +254,17 @@ func newConfig() *codersdk.DeploymentConfig {
|
|||
Flag: "oidc-username-field",
|
||||
Default: "preferred_username",
|
||||
},
|
||||
SignInText: &codersdk.DeploymentConfigField[string]{
|
||||
Name: "OpenID Connect sign in text",
|
||||
Usage: "The text to show on the OpenID Connect sign in button",
|
||||
Flag: "oidc-sign-in-text",
|
||||
Default: "OpenID Connect",
|
||||
},
|
||||
IconURL: &codersdk.DeploymentConfigField[string]{
|
||||
Name: "OpenID connect icon URL",
|
||||
Usage: "URL pointing to the icon to use on the OepnID Connect login button",
|
||||
Flag: "oidc-icon-url",
|
||||
},
|
||||
},
|
||||
|
||||
Telemetry: &codersdk.TelemetryConfig{
|
||||
|
|
|
@ -552,6 +552,8 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
|
|||
EmailDomain: cfg.OIDC.EmailDomain.Value,
|
||||
AllowSignups: cfg.OIDC.AllowSignups.Value,
|
||||
UsernameField: cfg.OIDC.UsernameField.Value,
|
||||
SignInText: cfg.OIDC.SignInText.Value,
|
||||
IconURL: cfg.OIDC.IconURL.Value,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -148,6 +148,9 @@ Flags:
|
|||
--oidc-email-domain strings Email domains that clients logging in
|
||||
with OIDC must match.
|
||||
Consumes $CODER_OIDC_EMAIL_DOMAIN
|
||||
--oidc-icon-url string URL pointing to the icon to use on the
|
||||
OepnID Connect login button
|
||||
Consumes $CODER_OIDC_ICON_URL
|
||||
--oidc-ignore-email-verified Ignore the email_verified claim from the
|
||||
upstream provider.
|
||||
Consumes $CODER_OIDC_IGNORE_EMAIL_VERIFIED
|
||||
|
@ -157,6 +160,10 @@ Flags:
|
|||
OIDC.
|
||||
Consumes $CODER_OIDC_SCOPES (default
|
||||
[openid,profile,email])
|
||||
--oidc-sign-in-text string The text to show on the OpenID Connect
|
||||
sign in button
|
||||
Consumes $CODER_OIDC_SIGN_IN_TEXT
|
||||
(default "OpenID Connect")
|
||||
--oidc-username-field string OIDC claim field to use as the username.
|
||||
Consumes $CODER_OIDC_USERNAME_FIELD
|
||||
(default "preferred_username")
|
||||
|
|
|
@ -5444,17 +5444,25 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AuthMethod": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AuthMethods": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"github": {
|
||||
"type": "boolean"
|
||||
"$ref": "#/definitions/codersdk.AuthMethod"
|
||||
},
|
||||
"oidc": {
|
||||
"type": "boolean"
|
||||
"$ref": "#/definitions/codersdk.OIDCAuthMethod"
|
||||
},
|
||||
"password": {
|
||||
"type": "boolean"
|
||||
"$ref": "#/definitions/codersdk.AuthMethod"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -6626,6 +6634,20 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"codersdk.OIDCAuthMethod": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"iconUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"signInText": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.OIDCConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -6641,6 +6663,9 @@ const docTemplate = `{
|
|||
"email_domain": {
|
||||
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
|
||||
},
|
||||
"icon_url": {
|
||||
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
|
||||
},
|
||||
"ignore_email_verified": {
|
||||
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
|
||||
},
|
||||
|
@ -6650,6 +6675,9 @@ const docTemplate = `{
|
|||
"scopes": {
|
||||
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
|
||||
},
|
||||
"sign_in_text": {
|
||||
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
|
||||
},
|
||||
"username_field": {
|
||||
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
|
||||
}
|
||||
|
|
|
@ -4825,17 +4825,25 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AuthMethod": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AuthMethods": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"github": {
|
||||
"type": "boolean"
|
||||
"$ref": "#/definitions/codersdk.AuthMethod"
|
||||
},
|
||||
"oidc": {
|
||||
"type": "boolean"
|
||||
"$ref": "#/definitions/codersdk.OIDCAuthMethod"
|
||||
},
|
||||
"password": {
|
||||
"type": "boolean"
|
||||
"$ref": "#/definitions/codersdk.AuthMethod"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -5927,6 +5935,20 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"codersdk.OIDCAuthMethod": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"iconUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"signInText": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.OIDCConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -5942,6 +5964,9 @@
|
|||
"email_domain": {
|
||||
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
|
||||
},
|
||||
"icon_url": {
|
||||
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
|
||||
},
|
||||
"ignore_email_verified": {
|
||||
"$ref": "#/definitions/codersdk.DeploymentConfigField-bool"
|
||||
},
|
||||
|
@ -5951,6 +5976,9 @@
|
|||
"scopes": {
|
||||
"$ref": "#/definitions/codersdk.DeploymentConfigField-array_string"
|
||||
},
|
||||
"sign_in_text": {
|
||||
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
|
||||
},
|
||||
"username_field": {
|
||||
"$ref": "#/definitions/codersdk.DeploymentConfigField-string"
|
||||
}
|
||||
|
|
|
@ -51,10 +51,24 @@ type GithubOAuth2Config struct {
|
|||
// @Success 200 {object} codersdk.AuthMethods
|
||||
// @Router /users/authmethods [get]
|
||||
func (api *API) userAuthMethods(rw http.ResponseWriter, r *http.Request) {
|
||||
var signInText string
|
||||
var iconURL string
|
||||
|
||||
if api.OIDCConfig != nil {
|
||||
signInText = api.OIDCConfig.SignInText
|
||||
}
|
||||
if api.OIDCConfig != nil {
|
||||
iconURL = api.OIDCConfig.IconURL
|
||||
}
|
||||
|
||||
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.AuthMethods{
|
||||
Password: true,
|
||||
Github: api.GithubOAuth2Config != nil,
|
||||
OIDC: api.OIDCConfig != nil,
|
||||
Password: codersdk.AuthMethod{Enabled: true},
|
||||
Github: codersdk.AuthMethod{Enabled: api.GithubOAuth2Config != nil},
|
||||
OIDC: codersdk.OIDCAuthMethod{
|
||||
AuthMethod: codersdk.AuthMethod{Enabled: api.OIDCConfig != nil},
|
||||
SignInText: signInText,
|
||||
IconURL: iconURL,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -215,6 +229,10 @@ type OIDCConfig struct {
|
|||
// UsernameField selects the claim field to be used as the created user's
|
||||
// username.
|
||||
UsernameField string
|
||||
// SignInText is the text to display on the OIDC login button
|
||||
SignInText string
|
||||
// IconURL points to the URL of an icon to display on the OIDC login button
|
||||
IconURL string
|
||||
}
|
||||
|
||||
// @Summary OpenID Connect Callback
|
||||
|
|
|
@ -77,8 +77,8 @@ func TestUserAuthMethods(t *testing.T) {
|
|||
|
||||
methods, err := client.AuthMethods(ctx)
|
||||
require.NoError(t, err)
|
||||
require.True(t, methods.Password)
|
||||
require.False(t, methods.Github)
|
||||
require.True(t, methods.Password.Enabled)
|
||||
require.False(t, methods.Github.Enabled)
|
||||
})
|
||||
t.Run("Github", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
@ -91,8 +91,8 @@ func TestUserAuthMethods(t *testing.T) {
|
|||
|
||||
methods, err := client.AuthMethods(ctx)
|
||||
require.NoError(t, err)
|
||||
require.True(t, methods.Password)
|
||||
require.True(t, methods.Github)
|
||||
require.True(t, methods.Password.Enabled)
|
||||
require.True(t, methods.Github.Enabled)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -200,6 +200,8 @@ type OIDCConfig struct {
|
|||
Scopes *DeploymentConfigField[[]string] `json:"scopes" typescript:",notnull"`
|
||||
IgnoreEmailVerified *DeploymentConfigField[bool] `json:"ignore_email_verified" typescript:",notnull"`
|
||||
UsernameField *DeploymentConfigField[string] `json:"username_field" typescript:",notnull"`
|
||||
SignInText *DeploymentConfigField[string] `json:"sign_in_text" typescript:",notnull"`
|
||||
IconURL *DeploymentConfigField[string] `json:"icon_url" typescript:",notnull"`
|
||||
}
|
||||
|
||||
type TelemetryConfig struct {
|
||||
|
|
|
@ -105,11 +105,21 @@ type CreateOrganizationRequest struct {
|
|||
Name string `json:"name" validate:"required,username"`
|
||||
}
|
||||
|
||||
// AuthMethods contains whether authentication types are enabled or not.
|
||||
// AuthMethods contains authentication method information like whether they are enabled or not or custom text, etc.
|
||||
type AuthMethods struct {
|
||||
Password bool `json:"password"`
|
||||
Github bool `json:"github"`
|
||||
OIDC bool `json:"oidc"`
|
||||
Password AuthMethod `json:"password"`
|
||||
Github AuthMethod `json:"github"`
|
||||
OIDC OIDCAuthMethod `json:"oidc"`
|
||||
}
|
||||
|
||||
type AuthMethod struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type OIDCAuthMethod struct {
|
||||
AuthMethod
|
||||
SignInText string `json:"signInText"`
|
||||
IconURL string `json:"iconUrl"`
|
||||
}
|
||||
|
||||
// HasFirstUser returns whether the first user has been created.
|
||||
|
|
|
@ -131,6 +131,13 @@ CODER_OIDC_IGNORE_EMAIL_VERIFIED=true
|
|||
|
||||
When a new user is created, the `preferred_username` claim becomes the username. If this claim is empty, the email address will be stripped of the domain, and become the username (e.g. `example@coder.com` becomes `example`).
|
||||
|
||||
If you'd like to change the OpenID Connect button text and/or icon, you can configure them like so:
|
||||
|
||||
```console
|
||||
CODER_OIDC_SIGN_IN_TEXT="Sign in with Gitea"
|
||||
CODER_OIDC_ICON_URL=https://gitea.io/images/gitea.png
|
||||
```
|
||||
|
||||
## SCIM (enterprise)
|
||||
|
||||
Coder supports user provisioning and deprovisioning via SCIM 2.0 with header
|
||||
|
|
|
@ -562,6 +562,17 @@ curl -X GET http://coder-server:8080/api/v2/config/deployment \
|
|||
"usage": "string",
|
||||
"value": ["string"]
|
||||
},
|
||||
"icon_url": {
|
||||
"default": "string",
|
||||
"enterprise": true,
|
||||
"flag": "string",
|
||||
"hidden": true,
|
||||
"name": "string",
|
||||
"secret": true,
|
||||
"shorthand": "string",
|
||||
"usage": "string",
|
||||
"value": "string"
|
||||
},
|
||||
"ignore_email_verified": {
|
||||
"default": true,
|
||||
"enterprise": true,
|
||||
|
@ -595,6 +606,17 @@ curl -X GET http://coder-server:8080/api/v2/config/deployment \
|
|||
"usage": "string",
|
||||
"value": ["string"]
|
||||
},
|
||||
"sign_in_text": {
|
||||
"default": "string",
|
||||
"enterprise": true,
|
||||
"flag": "string",
|
||||
"hidden": true,
|
||||
"name": "string",
|
||||
"secret": true,
|
||||
"shorthand": "string",
|
||||
"usage": "string",
|
||||
"value": "string"
|
||||
},
|
||||
"username_field": {
|
||||
"default": "string",
|
||||
"enterprise": true,
|
||||
|
|
|
@ -663,23 +663,45 @@
|
|||
| `audit_logs` | array of [codersdk.AuditLog](#codersdkauditlog) | false | | |
|
||||
| `count` | integer | false | | |
|
||||
|
||||
## codersdk.AuthMethods
|
||||
## codersdk.AuthMethod
|
||||
|
||||
```json
|
||||
{
|
||||
"github": true,
|
||||
"oidc": true,
|
||||
"password": true
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ---------- | ------- | -------- | ------------ | ----------- |
|
||||
| `github` | boolean | false | | |
|
||||
| `oidc` | boolean | false | | |
|
||||
| `password` | boolean | false | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| --------- | ------- | -------- | ------------ | ----------- |
|
||||
| `enabled` | boolean | false | | |
|
||||
|
||||
## codersdk.AuthMethods
|
||||
|
||||
```json
|
||||
{
|
||||
"github": {
|
||||
"enabled": true
|
||||
},
|
||||
"oidc": {
|
||||
"enabled": true,
|
||||
"iconUrl": "string",
|
||||
"signInText": "string"
|
||||
},
|
||||
"password": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ---------- | -------------------------------------------------- | -------- | ------------ | ----------- |
|
||||
| `github` | [codersdk.AuthMethod](#codersdkauthmethod) | false | | |
|
||||
| `oidc` | [codersdk.OIDCAuthMethod](#codersdkoidcauthmethod) | false | | |
|
||||
| `password` | [codersdk.AuthMethod](#codersdkauthmethod) | false | | |
|
||||
|
||||
## codersdk.AuthorizationCheck
|
||||
|
||||
|
@ -1898,6 +1920,17 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
|||
"usage": "string",
|
||||
"value": ["string"]
|
||||
},
|
||||
"icon_url": {
|
||||
"default": "string",
|
||||
"enterprise": true,
|
||||
"flag": "string",
|
||||
"hidden": true,
|
||||
"name": "string",
|
||||
"secret": true,
|
||||
"shorthand": "string",
|
||||
"usage": "string",
|
||||
"value": "string"
|
||||
},
|
||||
"ignore_email_verified": {
|
||||
"default": true,
|
||||
"enterprise": true,
|
||||
|
@ -1931,6 +1964,17 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
|||
"usage": "string",
|
||||
"value": ["string"]
|
||||
},
|
||||
"sign_in_text": {
|
||||
"default": "string",
|
||||
"enterprise": true,
|
||||
"flag": "string",
|
||||
"hidden": true,
|
||||
"name": "string",
|
||||
"secret": true,
|
||||
"shorthand": "string",
|
||||
"usage": "string",
|
||||
"value": "string"
|
||||
},
|
||||
"username_field": {
|
||||
"default": "string",
|
||||
"enterprise": true,
|
||||
|
@ -3192,6 +3236,24 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
|||
| `client_secret` | [codersdk.DeploymentConfigField-string](#codersdkdeploymentconfigfield-string) | false | | |
|
||||
| `enterprise_base_url` | [codersdk.DeploymentConfigField-string](#codersdkdeploymentconfigfield-string) | false | | |
|
||||
|
||||
## codersdk.OIDCAuthMethod
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"iconUrl": "string",
|
||||
"signInText": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ------------ | ------- | -------- | ------------ | ----------- |
|
||||
| `enabled` | boolean | false | | |
|
||||
| `iconUrl` | string | false | | |
|
||||
| `signInText` | string | false | | |
|
||||
|
||||
## codersdk.OIDCConfig
|
||||
|
||||
```json
|
||||
|
@ -3240,6 +3302,17 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
|||
"usage": "string",
|
||||
"value": ["string"]
|
||||
},
|
||||
"icon_url": {
|
||||
"default": "string",
|
||||
"enterprise": true,
|
||||
"flag": "string",
|
||||
"hidden": true,
|
||||
"name": "string",
|
||||
"secret": true,
|
||||
"shorthand": "string",
|
||||
"usage": "string",
|
||||
"value": "string"
|
||||
},
|
||||
"ignore_email_verified": {
|
||||
"default": true,
|
||||
"enterprise": true,
|
||||
|
@ -3273,6 +3346,17 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
|||
"usage": "string",
|
||||
"value": ["string"]
|
||||
},
|
||||
"sign_in_text": {
|
||||
"default": "string",
|
||||
"enterprise": true,
|
||||
"flag": "string",
|
||||
"hidden": true,
|
||||
"name": "string",
|
||||
"secret": true,
|
||||
"shorthand": "string",
|
||||
"usage": "string",
|
||||
"value": "string"
|
||||
},
|
||||
"username_field": {
|
||||
"default": "string",
|
||||
"enterprise": true,
|
||||
|
@ -3295,9 +3379,11 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
|||
| `client_id` | [codersdk.DeploymentConfigField-string](#codersdkdeploymentconfigfield-string) | false | | |
|
||||
| `client_secret` | [codersdk.DeploymentConfigField-string](#codersdkdeploymentconfigfield-string) | false | | |
|
||||
| `email_domain` | [codersdk.DeploymentConfigField-array_string](#codersdkdeploymentconfigfield-array_string) | false | | |
|
||||
| `icon_url` | [codersdk.DeploymentConfigField-string](#codersdkdeploymentconfigfield-string) | false | | |
|
||||
| `ignore_email_verified` | [codersdk.DeploymentConfigField-bool](#codersdkdeploymentconfigfield-bool) | false | | |
|
||||
| `issuer_url` | [codersdk.DeploymentConfigField-string](#codersdkdeploymentconfigfield-string) | false | | |
|
||||
| `scopes` | [codersdk.DeploymentConfigField-array_string](#codersdkdeploymentconfigfield-array_string) | false | | |
|
||||
| `sign_in_text` | [codersdk.DeploymentConfigField-string](#codersdkdeploymentconfigfield-string) | false | | |
|
||||
| `username_field` | [codersdk.DeploymentConfigField-string](#codersdkdeploymentconfigfield-string) | false | | |
|
||||
|
||||
## codersdk.Organization
|
||||
|
|
|
@ -139,9 +139,17 @@ curl -X GET http://coder-server:8080/api/v2/users/authmethods \
|
|||
|
||||
```json
|
||||
{
|
||||
"github": true,
|
||||
"oidc": true,
|
||||
"password": true
|
||||
"github": {
|
||||
"enabled": true
|
||||
},
|
||||
"oidc": {
|
||||
"enabled": true,
|
||||
"iconUrl": "string",
|
||||
"signInText": "string"
|
||||
},
|
||||
"password": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -72,12 +72,16 @@ coder server [flags]
|
|||
Consumes $CODER_OIDC_CLIENT_SECRET
|
||||
--oidc-email-domain strings Email domains that clients logging in with OIDC must match.
|
||||
Consumes $CODER_OIDC_EMAIL_DOMAIN
|
||||
--oidc-icon-url string URL pointing to the icon to use on the OepnID Connect login button
|
||||
Consumes $CODER_OIDC_ICON_URL
|
||||
--oidc-ignore-email-verified Ignore the email_verified claim from the upstream provider.
|
||||
Consumes $CODER_OIDC_IGNORE_EMAIL_VERIFIED
|
||||
--oidc-issuer-url string Issuer URL to use for Login with OIDC.
|
||||
Consumes $CODER_OIDC_ISSUER_URL
|
||||
--oidc-scopes strings Scopes to grant when authenticating with OIDC.
|
||||
Consumes $CODER_OIDC_SCOPES (default [openid,profile,email])
|
||||
--oidc-sign-in-text string The text to show on the OpenID Connect sign in button
|
||||
Consumes $CODER_OIDC_SIGN_IN_TEXT (default "OpenID Connect")
|
||||
--oidc-username-field string OIDC claim field to use as the username.
|
||||
Consumes $CODER_OIDC_USERNAME_FIELD (default "preferred_username")
|
||||
--postgres-url string URL of a PostgreSQL database. If empty, PostgreSQL binaries will be downloaded from Maven (https://repo1.maven.org/maven2) and store all data in the config root. Access the built-in database with "coder server postgres-builtin-url".
|
||||
|
|
|
@ -88,11 +88,16 @@ export interface AuditLogsRequest extends Pagination {
|
|||
readonly q?: string
|
||||
}
|
||||
|
||||
// From codersdk/users.go
|
||||
export interface AuthMethod {
|
||||
readonly enabled: boolean
|
||||
}
|
||||
|
||||
// From codersdk/users.go
|
||||
export interface AuthMethods {
|
||||
readonly password: boolean
|
||||
readonly github: boolean
|
||||
readonly oidc: boolean
|
||||
readonly password: AuthMethod
|
||||
readonly github: AuthMethod
|
||||
readonly oidc: OIDCAuthMethod
|
||||
}
|
||||
|
||||
// From codersdk/authorization.go
|
||||
|
@ -454,6 +459,12 @@ export interface OAuth2GithubConfig {
|
|||
readonly enterprise_base_url: DeploymentConfigField<string>
|
||||
}
|
||||
|
||||
// From codersdk/users.go
|
||||
export interface OIDCAuthMethod extends AuthMethod {
|
||||
readonly signInText: string
|
||||
readonly iconUrl: string
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export interface OIDCConfig {
|
||||
readonly allow_signups: DeploymentConfigField<boolean>
|
||||
|
@ -464,6 +475,8 @@ export interface OIDCConfig {
|
|||
readonly scopes: DeploymentConfigField<string[]>
|
||||
readonly ignore_email_verified: DeploymentConfigField<boolean>
|
||||
readonly username_field: DeploymentConfigField<string>
|
||||
readonly sign_in_text: DeploymentConfigField<string>
|
||||
readonly icon_url: DeploymentConfigField<string>
|
||||
}
|
||||
|
||||
// From codersdk/organizations.go
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
import Link from "@material-ui/core/Link"
|
||||
import Button from "@material-ui/core/Button"
|
||||
import GitHubIcon from "@material-ui/icons/GitHub"
|
||||
import KeyIcon from "@material-ui/icons/VpnKey"
|
||||
import Box from "@material-ui/core/Box"
|
||||
import { Language } from "./SignInForm"
|
||||
import { AuthMethods } from "../../api/typesGenerated"
|
||||
import { FC } from "react"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
|
||||
type OAuthSignInFormProps = {
|
||||
isLoading: boolean
|
||||
redirectTo: string
|
||||
authMethods?: AuthMethods
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
buttonIcon: {
|
||||
width: 14,
|
||||
height: 14,
|
||||
},
|
||||
}))
|
||||
|
||||
export const OAuthSignInForm: FC<OAuthSignInFormProps> = ({
|
||||
isLoading,
|
||||
redirectTo,
|
||||
authMethods,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<Box display="grid" gridGap="16px">
|
||||
{authMethods?.github.enabled && (
|
||||
<Link
|
||||
underline="none"
|
||||
href={`/api/v2/users/oauth2/github/callback?redirect=${encodeURIComponent(
|
||||
redirectTo,
|
||||
)}`}
|
||||
>
|
||||
<Button
|
||||
startIcon={<GitHubIcon className={styles.buttonIcon} />}
|
||||
disabled={isLoading}
|
||||
fullWidth
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
{Language.githubSignIn}
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{authMethods?.oidc.enabled && (
|
||||
<Link
|
||||
underline="none"
|
||||
href={`/api/v2/users/oidc/callback?redirect=${encodeURIComponent(
|
||||
redirectTo,
|
||||
)}`}
|
||||
>
|
||||
<Button
|
||||
startIcon={
|
||||
authMethods.oidc.iconUrl ? (
|
||||
<img
|
||||
alt="Open ID Connect icon"
|
||||
src={authMethods.oidc.iconUrl}
|
||||
width="24"
|
||||
height="24"
|
||||
/>
|
||||
) : (
|
||||
<KeyIcon className={styles.buttonIcon} />
|
||||
)
|
||||
}
|
||||
disabled={isLoading}
|
||||
fullWidth
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
{authMethods.oidc.signInText || Language.oidcSignIn}
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
import { Stack } from "../Stack/Stack"
|
||||
import { AlertBanner } from "../AlertBanner/AlertBanner"
|
||||
import TextField from "@material-ui/core/TextField"
|
||||
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
|
||||
import { LoadingButton } from "../LoadingButton/LoadingButton"
|
||||
import { Language, LoginErrors } from "./SignInForm"
|
||||
import { FormikContextType, FormikTouched, useFormik } from "formik"
|
||||
import * as Yup from "yup"
|
||||
import { FC } from "react"
|
||||
import { BuiltInAuthFormValues } from "./SignInForm.types"
|
||||
|
||||
type PasswordSignInFormProps = {
|
||||
loginErrors: Partial<Record<LoginErrors, Error | unknown>>
|
||||
onSubmit: (credentials: { email: string; password: string }) => void
|
||||
initialTouched?: FormikTouched<BuiltInAuthFormValues>
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
export const PasswordSignInForm: FC<PasswordSignInFormProps> = ({
|
||||
loginErrors,
|
||||
onSubmit,
|
||||
initialTouched,
|
||||
isLoading,
|
||||
}) => {
|
||||
const validationSchema = Yup.object({
|
||||
email: Yup.string()
|
||||
.trim()
|
||||
.email(Language.emailInvalid)
|
||||
.required(Language.emailRequired),
|
||||
password: Yup.string(),
|
||||
})
|
||||
|
||||
const form: FormikContextType<BuiltInAuthFormValues> =
|
||||
useFormik<BuiltInAuthFormValues>({
|
||||
initialValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
validationSchema,
|
||||
// The email field has an autoFocus, but users may log in with a button click.
|
||||
// This is set to `false` in order to keep the autoFocus, validateOnChange
|
||||
// and Formik experience friendly. Validation will kick in onChange (any
|
||||
// field), or after a submission attempt.
|
||||
validateOnBlur: false,
|
||||
onSubmit,
|
||||
initialTouched,
|
||||
})
|
||||
const getFieldHelpers = getFormHelpers<BuiltInAuthFormValues>(
|
||||
form,
|
||||
loginErrors.authError,
|
||||
)
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit}>
|
||||
<Stack>
|
||||
{Object.keys(loginErrors).map(
|
||||
(errorKey: string) =>
|
||||
Boolean(loginErrors[errorKey as LoginErrors]) && (
|
||||
<AlertBanner
|
||||
key={errorKey}
|
||||
severity="error"
|
||||
error={loginErrors[errorKey as LoginErrors]}
|
||||
text={Language.errorMessages[errorKey as LoginErrors]}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
<TextField
|
||||
{...getFieldHelpers("email")}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
autoFocus
|
||||
autoComplete="email"
|
||||
fullWidth
|
||||
label={Language.emailLabel}
|
||||
type="email"
|
||||
variant="outlined"
|
||||
/>
|
||||
<TextField
|
||||
{...getFieldHelpers("password")}
|
||||
autoComplete="current-password"
|
||||
fullWidth
|
||||
id="password"
|
||||
label={Language.passwordLabel}
|
||||
type="password"
|
||||
variant="outlined"
|
||||
/>
|
||||
<div>
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
fullWidth
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
{isLoading ? "" : Language.passwordSignIn}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</Stack>
|
||||
</form>
|
||||
)
|
||||
}
|
|
@ -29,8 +29,9 @@ Loading.args = {
|
|||
...SignedOut.args,
|
||||
isLoading: true,
|
||||
authMethods: {
|
||||
github: true,
|
||||
password: true,
|
||||
password: { enabled: true },
|
||||
github: { enabled: true },
|
||||
oidc: { enabled: false, signInText: "", iconUrl: "" },
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -99,9 +100,9 @@ export const WithGithub = Template.bind({})
|
|||
WithGithub.args = {
|
||||
...SignedOut.args,
|
||||
authMethods: {
|
||||
password: true,
|
||||
github: true,
|
||||
oidc: false,
|
||||
password: { enabled: true },
|
||||
github: { enabled: true },
|
||||
oidc: { enabled: false, signInText: "", iconUrl: "" },
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -109,9 +110,9 @@ export const WithOIDC = Template.bind({})
|
|||
WithOIDC.args = {
|
||||
...SignedOut.args,
|
||||
authMethods: {
|
||||
password: true,
|
||||
github: false,
|
||||
oidc: true,
|
||||
password: { enabled: true },
|
||||
github: { enabled: false },
|
||||
oidc: { enabled: true, signInText: "", iconUrl: "" },
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -119,8 +120,8 @@ export const WithGithubAndOIDC = Template.bind({})
|
|||
WithGithubAndOIDC.args = {
|
||||
...SignedOut.args,
|
||||
authMethods: {
|
||||
password: true,
|
||||
github: true,
|
||||
oidc: true,
|
||||
password: { enabled: true },
|
||||
github: { enabled: true },
|
||||
oidc: { enabled: true, signInText: "", iconUrl: "" },
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,29 +1,13 @@
|
|||
import Box from "@material-ui/core/Box"
|
||||
import Button from "@material-ui/core/Button"
|
||||
import Link from "@material-ui/core/Link"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import TextField from "@material-ui/core/TextField"
|
||||
import GitHubIcon from "@material-ui/icons/GitHub"
|
||||
import KeyIcon from "@material-ui/icons/VpnKey"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import { FormikContextType, FormikTouched, useFormik } from "formik"
|
||||
import { FC } from "react"
|
||||
import * as Yup from "yup"
|
||||
import Typography from "@material-ui/core/Typography"
|
||||
import { FormikTouched } from "formik"
|
||||
import { FC, useState } from "react"
|
||||
import { AuthMethods } from "../../api/typesGenerated"
|
||||
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
|
||||
import { LoadingButton } from "./../LoadingButton/LoadingButton"
|
||||
import { AlertBanner } from "components/AlertBanner/AlertBanner"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
/**
|
||||
* BuiltInAuthFormValues describes a form using built-in (email/password)
|
||||
* authentication. This form may not always be present depending on external
|
||||
* auth providers available and administrative configurations
|
||||
*/
|
||||
interface BuiltInAuthFormValues {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
import { Maybe } from "../Conditionals/Maybe"
|
||||
import { PasswordSignInForm } from "./PasswordSignInForm"
|
||||
import { OAuthSignInForm } from "./OAuthSignInForm"
|
||||
import { BuiltInAuthFormValues } from "./SignInForm.types"
|
||||
|
||||
export enum LoginErrors {
|
||||
AUTH_ERROR = "authError",
|
||||
|
@ -48,14 +32,6 @@ export const Language = {
|
|||
oidcSignIn: "OpenID Connect",
|
||||
}
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
email: Yup.string()
|
||||
.trim()
|
||||
.email(Language.emailInvalid)
|
||||
.required(Language.emailRequired),
|
||||
password: Yup.string(),
|
||||
})
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
width: "100%",
|
||||
|
@ -71,10 +47,6 @@ const useStyles = makeStyles((theme) => ({
|
|||
fontWeight: 600,
|
||||
},
|
||||
},
|
||||
buttonIcon: {
|
||||
width: 14,
|
||||
height: 14,
|
||||
},
|
||||
divider: {
|
||||
paddingTop: theme.spacing(3),
|
||||
paddingBottom: theme.spacing(3),
|
||||
|
@ -94,6 +66,12 @@ const useStyles = makeStyles((theme) => ({
|
|||
fontSize: 12,
|
||||
letterSpacing: 1,
|
||||
},
|
||||
showPasswordLink: {
|
||||
cursor: "pointer",
|
||||
fontSize: 12,
|
||||
color: theme.palette.text.secondary,
|
||||
marginTop: 12,
|
||||
},
|
||||
}))
|
||||
|
||||
export interface SignInFormProps {
|
||||
|
@ -114,26 +92,14 @@ export const SignInForm: FC<React.PropsWithChildren<SignInFormProps>> = ({
|
|||
onSubmit,
|
||||
initialTouched,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const form: FormikContextType<BuiltInAuthFormValues> =
|
||||
useFormik<BuiltInAuthFormValues>({
|
||||
initialValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
validationSchema,
|
||||
// The email field has an autoFocus, but users may login with a button click.
|
||||
// This is set to `false` in order to keep the autoFocus, validateOnChange
|
||||
// and Formik experience friendly. Validation will kick in onChange (any
|
||||
// field), or after a submission attempt.
|
||||
validateOnBlur: false,
|
||||
onSubmit,
|
||||
initialTouched,
|
||||
})
|
||||
const getFieldHelpers = getFormHelpers<BuiltInAuthFormValues>(
|
||||
form,
|
||||
loginErrors.authError,
|
||||
const oAuthEnabled = Boolean(
|
||||
authMethods?.github.enabled || authMethods?.oidc.enabled,
|
||||
)
|
||||
|
||||
// Hide password auth by default if any OAuth method is enabled
|
||||
const [showPasswordAuth, setShowPasswordAuth] = useState(!oAuthEnabled)
|
||||
const styles = useStyles()
|
||||
|
||||
const commonTranslation = useTranslation("common")
|
||||
const loginPageTranslation = useTranslation("loginPage")
|
||||
|
||||
|
@ -143,99 +109,36 @@ export const SignInForm: FC<React.PropsWithChildren<SignInFormProps>> = ({
|
|||
{loginPageTranslation.t("signInTo")}{" "}
|
||||
<strong>{commonTranslation.t("coder")}</strong>
|
||||
</h1>
|
||||
<form onSubmit={form.handleSubmit}>
|
||||
<Stack>
|
||||
{Object.keys(loginErrors).map(
|
||||
(errorKey: string) =>
|
||||
Boolean(loginErrors[errorKey as LoginErrors]) && (
|
||||
<AlertBanner
|
||||
key={errorKey}
|
||||
severity="error"
|
||||
error={loginErrors[errorKey as LoginErrors]}
|
||||
text={Language.errorMessages[errorKey as LoginErrors]}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
<TextField
|
||||
{...getFieldHelpers("email")}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
autoFocus
|
||||
autoComplete="email"
|
||||
fullWidth
|
||||
label={Language.emailLabel}
|
||||
type="email"
|
||||
variant="outlined"
|
||||
/>
|
||||
<TextField
|
||||
{...getFieldHelpers("password")}
|
||||
autoComplete="current-password"
|
||||
fullWidth
|
||||
id="password"
|
||||
label={Language.passwordLabel}
|
||||
type="password"
|
||||
variant="outlined"
|
||||
/>
|
||||
<div>
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
fullWidth
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
{isLoading ? "" : Language.passwordSignIn}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</Stack>
|
||||
</form>
|
||||
{(authMethods?.github || authMethods?.oidc) && (
|
||||
<div>
|
||||
<div className={styles.divider}>
|
||||
<div className={styles.dividerLine} />
|
||||
<div className={styles.dividerLabel}>Or</div>
|
||||
<div className={styles.dividerLine} />
|
||||
</div>
|
||||
|
||||
<Box display="grid" gridGap="16px">
|
||||
{authMethods.github && (
|
||||
<Link
|
||||
underline="none"
|
||||
href={`/api/v2/users/oauth2/github/callback?redirect=${encodeURIComponent(
|
||||
redirectTo,
|
||||
)}`}
|
||||
>
|
||||
<Button
|
||||
startIcon={<GitHubIcon className={styles.buttonIcon} />}
|
||||
disabled={isLoading}
|
||||
fullWidth
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
{Language.githubSignIn}
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{authMethods.oidc && (
|
||||
<Link
|
||||
underline="none"
|
||||
href={`/api/v2/users/oidc/callback?redirect=${encodeURIComponent(
|
||||
redirectTo,
|
||||
)}`}
|
||||
>
|
||||
<Button
|
||||
startIcon={<KeyIcon className={styles.buttonIcon} />}
|
||||
disabled={isLoading}
|
||||
fullWidth
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
{Language.oidcSignIn}
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</Box>
|
||||
<Maybe condition={showPasswordAuth}>
|
||||
<PasswordSignInForm
|
||||
loginErrors={loginErrors}
|
||||
onSubmit={onSubmit}
|
||||
initialTouched={initialTouched}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</Maybe>
|
||||
<Maybe condition={showPasswordAuth && oAuthEnabled}>
|
||||
<div className={styles.divider}>
|
||||
<div className={styles.dividerLine} />
|
||||
<div className={styles.dividerLabel}>Or</div>
|
||||
<div className={styles.dividerLine} />
|
||||
</div>
|
||||
)}
|
||||
</Maybe>
|
||||
<Maybe condition={oAuthEnabled}>
|
||||
<OAuthSignInForm
|
||||
isLoading={isLoading}
|
||||
redirectTo={redirectTo}
|
||||
authMethods={authMethods}
|
||||
/>
|
||||
</Maybe>
|
||||
<Maybe condition={!showPasswordAuth}>
|
||||
<Typography
|
||||
className={styles.showPasswordLink}
|
||||
onClick={() => setShowPasswordAuth(true)}
|
||||
>
|
||||
{loginPageTranslation.t("showPassword")}
|
||||
</Typography>
|
||||
</Maybe>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* BuiltInAuthFormValues describes a form using built-in (email/password)
|
||||
* authentication. This form may not always be present depending on external
|
||||
* auth providers available and administrative configurations
|
||||
*/
|
||||
export interface BuiltInAuthFormValues {
|
||||
email: string
|
||||
password: string
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
{
|
||||
"signInTo": "Sign in to"
|
||||
"signInTo": "Sign in to",
|
||||
"showPassword": "Show password login"
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
} from "../../testHelpers/renderHelpers"
|
||||
import { server } from "../../testHelpers/server"
|
||||
import { LoginPage } from "./LoginPage"
|
||||
import * as TypesGen from "api/typesGenerated"
|
||||
|
||||
describe("LoginPage", () => {
|
||||
beforeEach(() => {
|
||||
|
@ -80,16 +81,16 @@ describe("LoginPage", () => {
|
|||
})
|
||||
|
||||
it("shows github authentication when enabled", async () => {
|
||||
const authMethods: TypesGen.AuthMethods = {
|
||||
password: { enabled: true },
|
||||
github: { enabled: true },
|
||||
oidc: { enabled: true, signInText: "", iconUrl: "" },
|
||||
}
|
||||
|
||||
// Given
|
||||
server.use(
|
||||
rest.get("/api/v2/users/authmethods", async (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
password: true,
|
||||
github: true,
|
||||
}),
|
||||
)
|
||||
return res(ctx.status(200), ctx.json(authMethods))
|
||||
}),
|
||||
)
|
||||
|
||||
|
@ -97,7 +98,7 @@ describe("LoginPage", () => {
|
|||
render(<LoginPage />)
|
||||
|
||||
// Then
|
||||
await screen.findByText(Language.passwordSignIn)
|
||||
expect(screen.queryByText(Language.passwordSignIn)).not.toBeInTheDocument()
|
||||
await screen.findByText(Language.githubSignIn)
|
||||
})
|
||||
|
||||
|
@ -120,4 +121,32 @@ describe("LoginPage", () => {
|
|||
// Then
|
||||
await screen.findByText("Setup")
|
||||
})
|
||||
|
||||
it("hides password authentication if OIDC/GitHub is enabled and displays on click", async () => {
|
||||
const authMethods: TypesGen.AuthMethods = {
|
||||
password: { enabled: true },
|
||||
github: { enabled: true },
|
||||
oidc: { enabled: true, signInText: "", iconUrl: "" },
|
||||
}
|
||||
|
||||
// Given
|
||||
server.use(
|
||||
rest.get("/api/v2/users/authmethods", async (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(authMethods))
|
||||
}),
|
||||
)
|
||||
|
||||
// When
|
||||
render(<LoginPage />)
|
||||
|
||||
// Then
|
||||
expect(screen.queryByText(Language.passwordSignIn)).not.toBeInTheDocument()
|
||||
await screen.findByText(Language.githubSignIn)
|
||||
|
||||
const showPasswordAuthLink = screen.getByText("Show password login")
|
||||
await userEvent.click(showPasswordAuthLink)
|
||||
|
||||
await screen.findByText(Language.passwordSignIn)
|
||||
await screen.findByText(Language.githubSignIn)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -646,9 +646,9 @@ export const MockUserAgent: Types.UserAgent = {
|
|||
}
|
||||
|
||||
export const MockAuthMethods: TypesGen.AuthMethods = {
|
||||
password: true,
|
||||
github: false,
|
||||
oidc: false,
|
||||
password: { enabled: true },
|
||||
github: { enabled: false },
|
||||
oidc: { enabled: false, signInText: "", iconUrl: "" },
|
||||
}
|
||||
|
||||
export const MockGitSSHKey: TypesGen.GitSSHKey = {
|
||||
|
|
Loading…
Reference in New Issue