@@ -10,6 +10,7 @@ import (
1010 "github.com/gotify/server/v2/auth"
1111 "github.com/gotify/server/v2/decaymap"
1212 "github.com/gotify/server/v2/mode"
13+ "github.com/gotify/server/v2/model"
1314 "github.com/gotify/server/v2/test"
1415 "github.com/gotify/server/v2/test/testdb"
1516 "github.com/stretchr/testify/assert"
@@ -19,6 +20,8 @@ import (
1920
2021var origGenClientToken = generateClientToken
2122
23+ const testIssuer = "https://idp.example.com"
24+
2225func TestOIDCSuite (t * testing.T ) {
2326 suite .Run (t , new (OIDCSuite ))
2427}
@@ -62,83 +65,172 @@ func (s *OIDCSuite) Test_GenerateState_Unique() {
6265 assert .NotEqual (s .T (), s1 , s2 )
6366}
6467
65- func (s * OIDCSuite ) Test_ResolveUser_ExistingUser () {
66- s .db .NewUserWithName (1 , "alice" )
68+ func (s * OIDCSuite ) Test_ResolveUser_ReturningUser_MatchedByOIDCID () {
69+ oidcID := testIssuer + "#sub-1"
70+ s .db .CreateUser (& model.User {ID : 1 , Name : "alice" , OIDCID : & oidcID })
6771
68- info := & oidc.UserInfo {Claims : map [string ]any {"preferred_username" : "alice" }}
69- user , status , err := s .a .resolveUser (info )
72+ // The username claim differs from the stored name; the binding still matches.
73+ info := & oidc.UserInfo {Subject : "sub-1" , Claims : map [string ]any {"preferred_username" : "renamed" }}
74+ user , status , err := s .a .resolveUser (testIssuer , info )
7075
7176 assert .NoError (s .T (), err )
7277 assert .Equal (s .T (), 0 , status )
78+ assert .Equal (s .T (), uint (1 ), user .ID )
7379 assert .Equal (s .T (), "alice" , user .Name )
80+ assert .False (s .T (), s .notified )
81+ }
82+
83+ func (s * OIDCSuite ) Test_ResolveUser_LinkByUsername_BindsExistingUser () {
84+ s .a .LinkByUsername = true
85+ s .db .NewUserWithName (1 , "alice" )
86+
87+ info := & oidc.UserInfo {Subject : "sub-1" , Claims : map [string ]any {"preferred_username" : "alice" }}
88+ user , _ , err := s .a .resolveUser (testIssuer , info )
89+
90+ assert .NoError (s .T (), err )
7491 assert .Equal (s .T (), uint (1 ), user .ID )
92+ assert .NotNil (s .T (), user .OIDCID )
93+ assert .Equal (s .T (), testIssuer + "#sub-1" , * user .OIDCID )
94+ // Binding an existing user is not a registration, so no notification.
7595 assert .False (s .T (), s .notified )
96+
97+ bound , err := s .db .GetUserByOIDC (testIssuer + "#sub-1" )
98+ assert .NoError (s .T (), err )
99+ assert .NotNil (s .T (), bound )
100+ assert .Equal (s .T (), uint (1 ), bound .ID )
101+ }
102+
103+ func (s * OIDCSuite ) Test_ResolveUser_InvalidIssuer () {
104+ s .db .NewUserWithName (1 , "alice" )
105+
106+ info := & oidc.UserInfo {Subject : "sub-1" , Claims : map [string ]any {"preferred_username" : "alice" }}
107+ _ , status , err := s .a .resolveUser ("://example.org" , info )
108+
109+ assert .EqualError (s .T (), err , `issuer url "://example.org" is not a valid url: parse "://example.org": missing protocol scheme` )
110+ assert .Equal (s .T (), 500 , status )
111+ }
112+
113+ func (s * OIDCSuite ) Test_ResolveUser_InvalidIssuer_containsFragment () {
114+ s .db .NewUserWithName (1 , "alice" )
115+
116+ info := & oidc.UserInfo {Subject : "sub-1" , Claims : map [string ]any {"preferred_username" : "alice" }}
117+ _ , status , err := s .a .resolveUser (testIssuer + "#" , info )
118+
119+ assert .EqualError (s .T (), err , `issuer url "https://idp.example.com#" may not contain a fragment` )
120+ assert .Equal (s .T (), 500 , status )
121+ }
122+
123+ func (s * OIDCSuite ) Test_ResolveUser_LinkDisabled_RejectsExistingUsername () {
124+ s .db .NewUserWithName (1 , "alice" )
125+
126+ info := & oidc.UserInfo {Subject : "sub-1" , Claims : map [string ]any {"preferred_username" : "alice" }}
127+ _ , status , err := s .a .resolveUser (testIssuer , info )
128+
129+ assert .EqualError (s .T (), err , "a local user with the username alice already exists and linking by username is disabled" )
130+ assert .Equal (s .T (), 403 , status )
131+
132+ // The existing user must not have been bound.
133+ user , _ := s .db .GetUserByName ("alice" )
134+ assert .Nil (s .T (), user .OIDCID )
135+ }
136+
137+ func (s * OIDCSuite ) Test_ResolveUser_LinkByUsername_RejectsDifferentIdentity () {
138+ s .a .LinkByUsername = true
139+ otherID := testIssuer + "#other-sub"
140+ s .db .CreateUser (& model.User {ID : 1 , Name : "alice" , OIDCID : & otherID })
141+
142+ info := & oidc.UserInfo {Subject : "sub-1" , Claims : map [string ]any {"preferred_username" : "alice" }}
143+ _ , status , err := s .a .resolveUser (testIssuer , info )
144+
145+ assert .EqualError (s .T (), err , "the user alice is already bound to a different OIDC identity" )
146+ assert .Equal (s .T (), 403 , status )
76147}
77148
78149func (s * OIDCSuite ) Test_ResolveUser_AutoRegister () {
79- info := & oidc.UserInfo {Claims : map [string ]any {"preferred_username" : "newuser" }}
80- user , status , err := s .a .resolveUser (info )
150+ info := & oidc.UserInfo {Subject : "sub-1" , Claims : map [string ]any {"preferred_username" : "newuser" }}
151+ user , status , err := s .a .resolveUser (testIssuer , info )
81152
82153 assert .NoError (s .T (), err )
83154 assert .Equal (s .T (), 0 , status )
84155 assert .Equal (s .T (), "newuser" , user .Name )
85156 assert .False (s .T (), user .Admin )
157+ assert .NotNil (s .T (), user .OIDCID )
158+ assert .Equal (s .T (), testIssuer + "#sub-1" , * user .OIDCID )
86159 assert .True (s .T (), s .notified )
87160
88- // verify persisted
89- dbUser , err := s .db .GetUserByName ( "newuser " )
161+ // Verify persisted and bound.
162+ dbUser , err := s .db .GetUserByOIDC ( testIssuer + "#sub-1 " )
90163 assert .NoError (s .T (), err )
91164 assert .NotNil (s .T (), dbUser )
165+ assert .Equal (s .T (), "newuser" , dbUser .Name )
92166}
93167
94168func (s * OIDCSuite ) Test_ResolveUser_AutoRegisterDisabled () {
95169 s .a .AutoRegister = false
96- info := & oidc.UserInfo {Claims : map [string ]any {"preferred_username" : "newuser" }}
170+ info := & oidc.UserInfo {Subject : "sub-1" , Claims : map [string ]any {"preferred_username" : "newuser" }}
97171
98- _ , status , err := s .a .resolveUser (info )
172+ _ , status , err := s .a .resolveUser (testIssuer , info )
99173
100- assert .Error (s .T (), err )
174+ assert .EqualError (s .T (), err , "user does not exist and auto-registration is disabled" )
101175 assert .Equal (s .T (), 403 , status )
102176 s .db .AssertUsernameNotExist ("newuser" )
103177}
104178
179+ func (s * OIDCSuite ) Test_ResolveUser_MissingIssuer () {
180+ info := & oidc.UserInfo {Subject : "sub-1" , Claims : map [string ]any {"preferred_username" : "newuser" }}
181+
182+ _ , status , err := s .a .resolveUser ("" , info )
183+
184+ assert .EqualError (s .T (), err , "issuer claim was empty" )
185+ assert .Equal (s .T (), 500 , status )
186+ }
187+
188+ func (s * OIDCSuite ) Test_ResolveUser_MissingSubject () {
189+ info := & oidc.UserInfo {Claims : map [string ]any {"preferred_username" : "newuser" }}
190+
191+ _ , status , err := s .a .resolveUser (testIssuer , info )
192+
193+ assert .EqualError (s .T (), err , "subject claim was empty" )
194+ assert .Equal (s .T (), 500 , status )
195+ }
196+
105197func (s * OIDCSuite ) Test_ResolveUser_MissingClaim () {
106- info := & oidc.UserInfo {Claims : map [string ]any {}}
198+ info := & oidc.UserInfo {Subject : "sub-1" , Claims : map [string ]any {}}
107199
108- _ , status , err := s .a .resolveUser (info )
200+ _ , status , err := s .a .resolveUser (testIssuer , info )
109201
110- assert .Error (s .T (), err )
202+ assert .EqualError (s .T (), err , `username claim "preferred_username" is missing` )
111203 assert .Equal (s .T (), 500 , status )
112- assert .Contains (s .T (), err .Error (), "preferred_username" )
113204}
114205
115206func (s * OIDCSuite ) Test_ResolveUser_EmptyClaim () {
116- info := & oidc.UserInfo {Claims : map [string ]any {"preferred_username" : "" }}
207+ info := & oidc.UserInfo {Subject : "sub-1" , Claims : map [string ]any {"preferred_username" : "" }}
117208
118- _ , status , err := s .a .resolveUser (info )
209+ _ , status , err := s .a .resolveUser (testIssuer , info )
119210
120- assert .Error (s .T (), err )
211+ assert .EqualError (s .T (), err , "username claim was empty" )
121212 assert .Equal (s .T (), 500 , status )
122213}
123214
124215func (s * OIDCSuite ) Test_ResolveUser_NilClaim () {
125- info := & oidc.UserInfo {Claims : map [string ]any {"preferred_username" : nil }}
216+ info := & oidc.UserInfo {Subject : "sub-1" , Claims : map [string ]any {"preferred_username" : nil }}
126217
127- _ , status , err := s .a .resolveUser (info )
218+ _ , status , err := s .a .resolveUser (testIssuer , info )
128219
129- assert .Error (s .T (), err )
220+ assert .EqualError (s .T (), err , "username claim was empty" )
130221 assert .Equal (s .T (), 500 , status )
131222}
132223
133224func (s * OIDCSuite ) Test_ResolveUser_CustomClaim () {
134225 s .a .UsernameClaim = "email"
135- s .db .NewUserWithName (1 , "alice@example.com" )
136226
137- info := & oidc.UserInfo {Claims : map [string ]any {"email" : "alice @example.com" }}
138- user , _ , err := s .a .resolveUser (info )
227+ info := & oidc.UserInfo {Subject : "sub-1" , Claims : map [string ]any {"email" : "new @example.com" }}
228+ user , status , err := s .a .resolveUser (testIssuer , info )
139229
140230 assert .NoError (s .T (), err )
141- assert .Equal (s .T (), "alice@example.com" , user .Name )
231+ assert .Equal (s .T (), 0 , status )
232+ assert .Equal (s .T (), "new@example.com" , user .Name )
233+ assert .NotNil (s .T (), user .OIDCID )
142234}
143235
144236// --- createClient ---
@@ -170,6 +262,7 @@ func (s *OIDCSuite) Test_ExternalAuthorizeHandler_MissingFields() {
170262 s .a .ExternalAuthorizeHandler (s .ctx )
171263
172264 assert .Equal (s .T (), 400 , s .recorder .Code )
265+ assert .Contains (s .T (), s .ctx .Errors .Last ().Error (), "'CodeChallenge' failed on the 'required' tag" )
173266}
174267
175268// --- ExternalTokenHandler ---
@@ -181,6 +274,7 @@ func (s *OIDCSuite) Test_ExternalTokenHandler_InvalidJSON() {
181274 s .a .ExternalTokenHandler (s .ctx )
182275
183276 assert .Equal (s .T (), 400 , s .recorder .Code )
277+ assert .Contains (s .T (), s .ctx .Errors .Last ().Error (), "invalid character" )
184278}
185279
186280func (s * OIDCSuite ) Test_ExternalTokenHandler_UnknownState () {
@@ -192,4 +286,5 @@ func (s *OIDCSuite) Test_ExternalTokenHandler_UnknownState() {
192286 s .a .ExternalTokenHandler (s .ctx )
193287
194288 assert .Equal (s .T (), 400 , s .recorder .Code )
289+ assert .EqualError (s .T (), s .ctx .Errors .Last (), "unknown or expired state" )
195290}
0 commit comments