diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000000..3550a30f2d
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/.gitignore b/.gitignore
index 46c8b9b49c..7889df77b1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -108,6 +108,9 @@ prime/
*_source.tar.bz2
.DS_Store
+# nix-direnv generated files
+.direnv/
+
# Make evidence files
/.make_evidence
diff --git a/modules/base/tool.go b/modules/base/tool.go
index 378eb7e3dd..9e43030f40 100644
--- a/modules/base/tool.go
+++ b/modules/base/tool.go
@@ -48,13 +48,10 @@ func BasicAuthDecode(encoded string) (string, string, error) {
return "", "", err
}
- auth := strings.SplitN(string(s), ":", 2)
-
- if len(auth) != 2 {
- return "", "", errors.New("invalid basic authentication")
+ if username, password, ok := strings.Cut(string(s), ":"); ok {
+ return username, password, nil
}
-
- return auth[0], auth[1], nil
+ return "", "", errors.New("invalid basic authentication")
}
// VerifyTimeLimitCode verify time limit code
diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go
index 62de7229ac..4af8b9bc4d 100644
--- a/modules/base/tool_test.go
+++ b/modules/base/tool_test.go
@@ -41,6 +41,9 @@ func TestBasicAuthDecode(t *testing.T) {
_, _, err = BasicAuthDecode("invalid")
assert.Error(t, err)
+
+ _, _, err = BasicAuthDecode("YWxpY2U=") // "alice", no colon
+ assert.Error(t, err)
}
func TestVerifyTimeLimitCode(t *testing.T) {
diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index 7a04ab4843..d17db1fcb4 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -387,6 +387,8 @@ relevant_repositories=Seuls les dépôts pertinents sont affichés, %s. Veuillez vérifier votre boîte de réception dans la prochaine %s pour terminer le processus d’inscription. Si votre adresse courriel est incorrecte, vous pouvez vous reconnecter et la modifier.
must_change_password=Réinitialisez votre mot de passe
@@ -455,6 +459,8 @@ sspi_auth_failed=Échec de l'authentification SSPI
password_pwned=Le mot de passe que vous avez choisi se trouve sur la liste des mots de passe ayant fuité sur internet. Veuillez réessayer avec un mot de passe différent et considérer remplacer ce mot de passe si vous l'utilisez ailleurs.
password_pwned_err=Impossible d'envoyer la demande à HaveIBeenPwned
last_admin=Vous ne pouvez pas supprimer ce compte car au moins un administrateur est requis.
+signin_passkey=Se connecter avec une clé d’identification (passkey)
+back_to_sign_in=Revenir à la page de connexion
[mail]
view_it_on=Voir sur %s
@@ -471,6 +477,7 @@ activate_email=Veuillez vérifier votre adresse courriel
activate_email.title=%s, veuillez vérifier votre adresse courriel
activate_email.text=Veuillez cliquer sur le lien suivant pour vérifier votre adresse courriel dans %s:
+register_notify=Bienvenue sur %s
register_notify.title=%[1]s, bienvenue à %[2]s
register_notify.text_1=ceci est votre courriel de confirmation d'inscription pour %s!
register_notify.text_2=Vous pouvez maintenant vous connecter avec le nom d'utilisateur : %s.
@@ -907,6 +914,7 @@ create_oauth2_application_success=Vous avez créé une nouvelle application OAut
update_oauth2_application_success=Vous avez mis à jour l'application OAuth2 avec succès.
oauth2_application_name=Nom de l'Application
oauth2_confidential_client=Client confidentiel. Sélectionnez cette option pour les applications qui préservent la confidentialité du secret, telles que les applications web. Ne la sélectionnez pas pour les applications natives, y compris les applications de bureau et les applications mobiles.
+oauth2_skip_secondary_authorization=Ne plus demander d’autorisation pour les clients publics après la première fois. Introduit un risque de sécurité.
oauth2_redirect_uris=URI de redirection. Veuillez utiliser une nouvelle ligne pour chaque URI.
save_application=Enregistrer
oauth2_client_id=ID du client
@@ -2273,6 +2281,7 @@ settings.event_wiki_desc=Page wiki créée, renommée, modifiée ou supprimée.
settings.event_release=Publication
settings.event_release_desc=Publication publiée, mise à jour ou supprimée.
settings.event_push=Soumission
+settings.event_force_push=Poussée forcée
settings.event_push_desc=Soumission Git.
settings.event_repository=Dépôt
settings.event_repository_desc=Dépôt créé ou supprimé.
@@ -2366,10 +2375,28 @@ settings.protect_this_branch=Activer la protection de branche
settings.protect_this_branch_desc=Empêche les suppressions et limite les poussées et fusions sur cette branche.
settings.protect_disable_push=Désactiver la soumission
settings.protect_disable_push_desc=Aucune soumission ne sera possible sur cette branche.
+settings.protect_disable_force_push=Désactiver les poussés forcées
+settings.protect_disable_force_push_desc=Aucune poussée forcée ne sera possible sur cette branche.
settings.protect_enable_push=Activer la soumission
settings.protect_enable_push_desc=Toute personne ayant un accès en écriture sera autorisée à soumettre sur cette branche (sans forcer).
+settings.protect_enable_force_push_all=Activer les poussées forcées
+settings.protect_enable_force_push_all_desc=Toute personne pouvant pousser pourra forcer sur cette branche.
+settings.protect_enable_force_push_allowlist=Soumission forcée sur autorisation uniquement
+settings.protect_enable_force_push_allowlist_desc=Seuls les utilisateurs ou équipes autorisés ayants un droit de pousser seront autorisés à pousser en force sur cette branche.
settings.protect_enable_merge=Activer la fusion
settings.protect_enable_merge_desc=Toute personne ayant un accès en écriture sera autorisée à fusionner les demandes d'ajout dans cette branche.
+settings.protect_whitelist_committers=Soumissions sur autorisation uniquement
+settings.protect_whitelist_committers_desc=Seuls les utilisateurs ou les équipes autorisés pourront pousser sur cette branche (sans forcer).
+settings.protect_whitelist_deploy_keys=Clés de déploiement pouvant écrire autorisées à pousser.
+settings.protect_whitelist_users=Utilisateurs autorisés à pousser :
+settings.protect_whitelist_teams=Équipes autorisées à pousser :
+settings.protect_force_push_allowlist_users=Utilisateurs autorisés à pousser en force :
+settings.protect_force_push_allowlist_teams=Équipes autorisées à pousser en force :
+settings.protect_force_push_allowlist_deploy_keys=Clés de déploiement pouvant pousser autorisées à pousser en force.
+settings.protect_merge_whitelist_committers=Activer la liste d’autorisés pour la fusion
+settings.protect_merge_whitelist_committers_desc=N’autoriser que les utilisateurs et les équipes listés à appliquer les demandes de fusion sur cette branche.
+settings.protect_merge_whitelist_users=Utilisateurs autorisés à fusionner :
+settings.protect_merge_whitelist_teams=Équipes autorisées à fusionner :
settings.protect_check_status_contexts=Activer le Contrôle Qualité
settings.protect_status_check_patterns=Motifs de vérification des statuts :
settings.protect_status_check_patterns_desc=Entrez des motifs pour spécifier quelles vérifications doivent réussir avant que des branches puissent être fusionnées. Un motif par ligne. Un motif ne peut être vide.
@@ -2380,6 +2407,10 @@ settings.protect_invalid_status_check_pattern=Motif de vérification des statuts
settings.protect_no_valid_status_check_patterns=Aucun motif de vérification des statuts valide.
settings.protect_required_approvals=Minimum d'approbations requis :
settings.protect_required_approvals_desc=Permet de fusionner les demandes d’ajout lorsque suffisamment d’évaluation sont positives.
+settings.protect_approvals_whitelist_enabled=Restreindre les approbations aux utilisateurs ou aux équipes sur liste d’autorisés
+settings.protect_approvals_whitelist_enabled_desc=Seuls les évaluations des utilisateurs ou des équipes suivantes compteront dans les approbations requises. Si laissé vide, les évaluations de toute personne ayant un accès en écriture seront comptabilisées à la place.
+settings.protect_approvals_whitelist_users=Évaluateurs autorisés :
+settings.protect_approvals_whitelist_teams=Équipes d’évaluateurs autorisés :
settings.dismiss_stale_approvals=Révoquer automatiquement les approbations périmées
settings.dismiss_stale_approvals_desc=Lorsque des nouvelles révisions changent le contenu de la demande d’ajout, les approbations existantes sont révoquées.
settings.ignore_stale_approvals=Ignorer les approbations obsolètes
diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index 0ccd460a78..204248d63f 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -5,7 +5,6 @@ package auth
import (
go_context "context"
- "encoding/base64"
"errors"
"fmt"
"html"
@@ -326,10 +325,29 @@ func getOAuthGroupsForUser(ctx go_context.Context, user *user_model.User) ([]str
return groups, nil
}
+func parseBasicAuth(ctx *context.Context) (username, password string, err error) {
+ authHeader := ctx.Req.Header.Get("Authorization")
+ if authType, authData, ok := strings.Cut(authHeader, " "); ok && authType == "Basic" {
+ return base.BasicAuthDecode(authData)
+ }
+ return "", "", errors.New("invalid basic authentication")
+}
+
// IntrospectOAuth introspects an oauth token
func IntrospectOAuth(ctx *context.Context) {
- if ctx.Doer == nil {
- ctx.Resp.Header().Set("WWW-Authenticate", `Bearer realm=""`)
+ clientIDValid := false
+ if clientID, clientSecret, err := parseBasicAuth(ctx); err == nil {
+ app, err := auth.GetOAuth2ApplicationByClientID(ctx, clientID)
+ if err != nil && !auth.IsErrOauthClientIDInvalid(err) {
+ // this is likely a database error; log it and respond without details
+ log.Error("Error retrieving client_id: %v", err)
+ ctx.Error(http.StatusInternalServerError)
+ return
+ }
+ clientIDValid = err == nil && app.ValidateClientSecret([]byte(clientSecret))
+ }
+ if !clientIDValid {
+ ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm=""`)
ctx.PlainText(http.StatusUnauthorized, "no valid authorization")
return
}
@@ -639,9 +657,8 @@ func AccessTokenOAuth(ctx *context.Context) {
// if there is no ClientID or ClientSecret in the request body, fill these fields by the Authorization header and ensure the provided field matches the Authorization header
if form.ClientID == "" || form.ClientSecret == "" {
authHeader := ctx.Req.Header.Get("Authorization")
- authContent := strings.SplitN(authHeader, " ", 2)
- if len(authContent) == 2 && authContent[0] == "Basic" {
- payload, err := base64.StdEncoding.DecodeString(authContent[1])
+ if authType, authData, ok := strings.Cut(authHeader, " "); ok && authType == "Basic" {
+ clientID, clientSecret, err := base.BasicAuthDecode(authData)
if err != nil {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
@@ -649,30 +666,23 @@ func AccessTokenOAuth(ctx *context.Context) {
})
return
}
- pair := strings.SplitN(string(payload), ":", 2)
- if len(pair) != 2 {
- handleAccessTokenError(ctx, AccessTokenError{
- ErrorCode: AccessTokenErrorCodeInvalidRequest,
- ErrorDescription: "cannot parse basic auth header",
- })
- return
- }
- if form.ClientID != "" && form.ClientID != pair[0] {
+ // validate that any fields present in the form match the Basic auth header
+ if form.ClientID != "" && form.ClientID != clientID {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "client_id in request body inconsistent with Authorization header",
})
return
}
- form.ClientID = pair[0]
- if form.ClientSecret != "" && form.ClientSecret != pair[1] {
+ form.ClientID = clientID
+ if form.ClientSecret != "" && form.ClientSecret != clientSecret {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "client_secret in request body inconsistent with Authorization header",
})
return
}
- form.ClientSecret = pair[1]
+ form.ClientSecret = clientSecret
}
}
diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go
index e9b69f5f14..c3f0abbe1d 100644
--- a/tests/integration/oauth_test.go
+++ b/tests/integration/oauth_test.go
@@ -419,3 +419,59 @@ func TestRefreshTokenInvalidation(t *testing.T) {
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
assert.Equal(t, "token was already used", parsedError.ErrorDescription)
}
+
+func TestOAuthIntrospection(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
+ "grant_type": "authorization_code",
+ "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
+ "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
+ "redirect_uri": "a",
+ "code": "authcode",
+ "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
+ })
+ resp := MakeRequest(t, req, http.StatusOK)
+ type response struct {
+ AccessToken string `json:"access_token"`
+ TokenType string `json:"token_type"`
+ ExpiresIn int64 `json:"expires_in"`
+ RefreshToken string `json:"refresh_token"`
+ }
+ parsed := new(response)
+
+ assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
+ assert.True(t, len(parsed.AccessToken) > 10)
+ assert.True(t, len(parsed.RefreshToken) > 10)
+
+ // successful request with a valid client_id/client_secret and a valid token
+ req = NewRequestWithValues(t, "POST", "/login/oauth/introspect", map[string]string{
+ "token": parsed.AccessToken,
+ })
+ req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9")
+ resp = MakeRequest(t, req, http.StatusOK)
+ type introspectResponse struct {
+ Active bool `json:"active"`
+ Scope string `json:"scope,omitempty"`
+ }
+ introspectParsed := new(introspectResponse)
+ assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), introspectParsed))
+ assert.True(t, introspectParsed.Active)
+
+ // successful request with a valid client_id/client_secret, but an invalid token
+ req = NewRequestWithValues(t, "POST", "/login/oauth/introspect", map[string]string{
+ "token": "xyzzy",
+ })
+ req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9")
+ resp = MakeRequest(t, req, http.StatusOK)
+ introspectParsed = new(introspectResponse)
+ assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), introspectParsed))
+ assert.False(t, introspectParsed.Active)
+
+ // unsuccessful request with an invalid client_id/client_secret
+ req = NewRequestWithValues(t, "POST", "/login/oauth/introspect", map[string]string{
+ "token": parsed.AccessToken,
+ })
+ req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpK")
+ resp = MakeRequest(t, req, http.StatusUnauthorized)
+ assert.Contains(t, resp.Body.String(), "no valid authorization")
+}
diff --git a/web_src/js/features/user-auth-webauthn.ts b/web_src/js/features/user-auth-webauthn.ts
index 7b7508c4f1..35c6aa8f7c 100644
--- a/web_src/js/features/user-auth-webauthn.ts
+++ b/web_src/js/features/user-auth-webauthn.ts
@@ -5,6 +5,10 @@ import {GET, POST} from '../modules/fetch.ts';
const {appSubUrl} = window.config;
export async function initUserAuthWebAuthn() {
+ if (!document.querySelector('.user.signin')) {
+ return;
+ }
+
if (!detectWebAuthnSupport()) {
return;
}