Skip to content

Commit bb4ae44

Browse files
committed
add oauth2 device flow support
Signed-off-by: a1012112796 <[email protected]>
1 parent aef4a35 commit bb4ae44

File tree

20 files changed

+1067
-34
lines changed

20 files changed

+1067
-34
lines changed

models/auth/oauth2.go

Lines changed: 392 additions & 16 deletions
Large diffs are not rendered by default.

models/auth/oauth2_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
package auth_test
55

66
import (
7+
"fmt"
78
"testing"
89

910
auth_model "code.gitea.io/gitea/models/auth"
11+
"code.gitea.io/gitea/models/db"
1012
"code.gitea.io/gitea/models/unittest"
1113

1214
"github.com/stretchr/testify/assert"
@@ -262,3 +264,48 @@ func TestOAuth2AuthorizationCode_Invalidate(t *testing.T) {
262264
func TestOAuth2AuthorizationCode_TableName(t *testing.T) {
263265
assert.Equal(t, "oauth2_authorization_code", new(auth_model.OAuth2AuthorizationCode).TableName())
264266
}
267+
268+
func TestOAuth2Device_GetDeviceByDeviceCode(t *testing.T) {
269+
assert.NoError(t, unittest.PrepareTestDatabase())
270+
271+
app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1})
272+
273+
device, err := app.CreateDevice(t.Context(), "")
274+
assert.NoError(t, err)
275+
276+
device2, err := app.GetDeviceByDeviceCode(t.Context(), device.DeviceCode)
277+
assert.NoError(t, err)
278+
assert.Equal(t, device.ID, device2.ID)
279+
}
280+
281+
func TestOAuth2Device_GetDeviceByUserCode(t *testing.T) {
282+
assert.NoError(t, unittest.PrepareTestDatabase())
283+
284+
app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1})
285+
286+
device, err := app.CreateDevice(t.Context(), "")
287+
assert.NoError(t, err)
288+
289+
device2, err := auth_model.GetDeviceByUserCode(t.Context(), device.UserCode)
290+
assert.NoError(t, err)
291+
assert.Equal(t, device.ID, device2.ID)
292+
293+
device2.CreateGrant(t.Context(), 1)
294+
295+
_, err = auth_model.GetDeviceByUserCode(t.Context(), device.UserCode)
296+
assert.EqualError(t, err, fmt.Sprintf("oauth2 device not found: [user_code: %s. id: 0]", device.UserCode))
297+
}
298+
299+
func TestOAuth2Device_GetDeviceByUserCode_Expired(t *testing.T) {
300+
assert.NoError(t, unittest.PrepareTestDatabase())
301+
302+
app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1})
303+
304+
device, err := app.CreateDevice(t.Context(), "")
305+
assert.NoError(t, err)
306+
307+
db.GetEngine(t.Context()).ID(device.ID).Cols("expired_unix").Update(&auth_model.OAuth2Device{ExpiredUnix: 1})
308+
309+
_, err = auth_model.GetDeviceByUserCode(t.Context(), device.UserCode)
310+
assert.EqualError(t, err, fmt.Sprintf("oauth2 device not found: [user_code: %s. id: 0]", device.UserCode))
311+
}

models/migrations/migrations.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ func prepareMigrationTasks() []*migration {
393393

394394
// Gitea 1.24.0 ends at database version 321
395395
newMigration(321, "Use LONGTEXT for some columns and fix review_state.updated_files column", v1_25.UseLongTextInSomeColumnsAndFixBugs),
396+
newMigration(322, "Add OAuth2 Device Flow Support", v1_25.AddOAuth2DeviceFlowSupport),
396397
}
397398
return preparedMigrations
398399
}

models/migrations/v1_25/v322.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_25
5+
6+
import (
7+
"code.gitea.io/gitea/modules/timeutil"
8+
"xorm.io/xorm"
9+
)
10+
11+
func AddOAuth2DeviceFlowSupport(x *xorm.Engine) error {
12+
type OAuth2Application struct {
13+
EnableDeviceFlow bool `xorm:"NOT NULL DEFAULT FALSE"`
14+
}
15+
16+
if err := x.Sync2(new(OAuth2Application)); err != nil {
17+
return err
18+
}
19+
20+
type OAuth2Device struct {
21+
ID int64 `xorm:"pk autoincr"`
22+
DeviceCode string `xorm:"-"`
23+
DeviceCodeHash string `xorm:"UNIQUE"` // sha256 of device code
24+
DeviceCodeSalt string
25+
DeviceCodeID string `xorm:"INDEX"`
26+
UserCode string `xorm:"INDEX VARCHAR(9)"`
27+
Application *OAuth2Application `xorm:"-"`
28+
ApplicationID int64 `xorm:"INDEX"`
29+
Scope string `xorm:"TEXT"`
30+
GrantID int64
31+
CreatedUnix timeutil.TimeStamp `xorm:"created"`
32+
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
33+
ExpiredUnix timeutil.TimeStamp
34+
}
35+
36+
if err := x.Sync2(new(OAuth2Device)); err != nil {
37+
return err
38+
}
39+
40+
type OAuth2DeviceGrant struct {
41+
ID int64 `xorm:"pk autoincr"`
42+
UserID int64 `xorm:"INDEX"`
43+
Application *OAuth2Application `xorm:"-"`
44+
ApplicationID int64 `xorm:"INDEX"`
45+
DeviceID int64 `xorm:"INDEX"`
46+
Counter int64 `xorm:"NOT NULL DEFAULT 1"`
47+
UserCode string `xorm:"VARCHAR(9)"`
48+
Scope string `xorm:"TEXT"`
49+
Nonce string `xorm:"TEXT"`
50+
CreatedUnix timeutil.TimeStamp `xorm:"created"`
51+
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
52+
}
53+
return x.Sync2(new(OAuth2DeviceGrant))
54+
}

modules/setting/oauth2.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ var OAuth2 = struct {
9898
JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
9999
MaxTokenLength int
100100
DefaultApplications []string
101+
DeviceFlowExpirationTime int64
101102
}{
102103
Enabled: true,
103104
AccessTokenExpirationTime: 3600,
@@ -107,6 +108,7 @@ var OAuth2 = struct {
107108
JWTSigningPrivateKeyFile: "jwt/private.pem",
108109
MaxTokenLength: math.MaxInt16,
109110
DefaultApplications: []string{"git-credential-oauth", "git-credential-manager", "tea"},
111+
DeviceFlowExpirationTime: 900,
110112
}
111113

112114
func loadOAuth2From(rootCfg ConfigProvider) {

options/locale/locale_en-US.ini

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,13 @@ password_pwned_err = Could not complete request to HaveIBeenPwned
489489
last_admin = You cannot remove the last admin. There must be at least one admin.
490490
signin_passkey = Sign in with a passkey
491491
back_to_sign_in = Back to Sign In
492+
authorize_device_title = Device Activation
493+
authorize_device_description = Enter the code displayed on your device
494+
device_not_found = Device not found
495+
device_found = Device found: %s
496+
device_expired = The code has been expired
497+
device_granted = The code has been used
498+
device_granted_success = Device Activation Successful (user code: %s)
492499
493500
[mail]
494501
view_it_on = View it on %s
@@ -962,13 +969,16 @@ oauth2_application_edit = Edit
962969
oauth2_application_create_description = OAuth2 applications gives your third-party application access to user accounts on this instance.
963970
oauth2_application_remove_description = Removing an OAuth2 application will prevent it from accessing authorized user accounts on this instance. Continue?
964971
oauth2_application_locked = Gitea pre-registers some OAuth2 applications on startup if enabled in config. To prevent unexpected behavior, these can neither be edited nor removed. Please refer to the OAuth2 documentation for more information.
972+
oauth2_enable_device_flow = Enable Device Flow
965973

966974
authorized_oauth2_applications = Authorized OAuth2 Applications
967975
authorized_oauth2_applications_description = You have granted access to your personal Gitea account to these third-party applications. Please revoke access for applications you no longer need.
976+
authorized_oauth2_device_description = You have granted access to your personal Gitea account to these third-party applications using device flow. Please revoke access for applications you no longer need.
968977
revoke_key = Revoke
969978
revoke_oauth2_grant = Revoke Access
970979
revoke_oauth2_grant_description = Revoking access for this third-party application will prevent this application from accessing your data. Are you sure?
971980
revoke_oauth2_grant_success = Access revoked successfully.
981+
authorized_oauth2_user_code = User code: %s
972982

973983
twofa_desc = To protect your account against password theft, you can use a smartphone or another device for receiving time-based one-time passwords ("TOTP").
974984
twofa_recovery_tip = If you lose your device, you will be able to use a single-use recovery key to regain access to your account.

0 commit comments

Comments
 (0)