-
Notifications
You must be signed in to change notification settings - Fork 0
/
hydra.go
2902 lines (2384 loc) · 92.5 KB
/
hydra.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package core
// See the markdown documentation for the login-, two-factor-, consent- and logout pages for a brief description.
import (
"crypto/tls"
"errors"
"fmt"
"net/http"
"net/url"
"runtime"
"strings"
"time"
"github.com/croessner/nauthilus/server/config"
errors2 "github.com/croessner/nauthilus/server/errors"
"github.com/croessner/nauthilus/server/global"
"github.com/croessner/nauthilus/server/logging"
"github.com/croessner/nauthilus/server/tags"
"github.com/croessner/nauthilus/server/util"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/go-kit/log/level"
"github.com/justinas/nosurf"
"github.com/nicksnyder/go-i18n/v2/i18n"
openapi "github.com/ory/hydra-client-go/v2"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/spf13/viper"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"golang.org/x/text/language/display"
_ "golang.org/x/text/message/catalog"
)
// Scope represents a scope used in the ConsentPageData struct. It contains the name and description of the scope.
// Scope represents the scope of an object.
type Scope struct {
// ScopeName represents the name of the scope.
ScopeName string
// ScopeDescription represents a detailed description of the scope.
ScopeDescription string
}
// Language represents a language used in various page data structs.
// Language represents a programming language
type Language struct {
// LanguageLink represents the link associated with the language
LanguageLink string
// LanguageName represents the name of the language
LanguageName string
}
type LoginPageData struct {
// InDevelopment is a flag that is true, if the build-tag dev is used.
InDevelopment bool
// Determines if the Welcome message should be displayed
WantWelcome bool
// Determines if the Policy should be displayed
WantPolicy bool
// Determines if the Terms of Service (TOS) should be displayed
WantTos bool
// Determines if the About information should be displayed
WantAbout bool
// WantRemember is a flag for the regular login page.
WantRemember bool
// Indicates if there was an error
HaveError bool
// The title of the Login page
Title string
// The Welcome message
Welcome string
// The path or URL to logo image to be displayed
LogoImage string
// The alternate text for the logo image
LogoImageAlt string
// The name of the application
ApplicationName string
// The login details
Login string
// The placeholder for the login input form
LoginPlaceholder string
// The Privacy statement
Privacy string
// User password
Password string
// Placeholder for password input form
PasswordPlaceholder string
// The Policy terms
Policy string
// The URL to the policy document
PolicyUri string
// The Terms of Service
Tos string
// The URL to the Terms of Service document
TosUri string
// Information about the service or company
About string
// The URL to more About information
AboutUri string
// Information regarding remember functionality
Remember string
// Text for Submit button
Submit string
// Error message if any
ErrorMessage string
// Alternate choices text
Or string
// Information on the device being used
Device string
// CSRF security token
CSRFToken string
// Login challenge token
LoginChallenge string
// Endpoint for submitting login
PostLoginEndpoint string
// Endpoint for device login
DeviceLoginEndpoint string
// Current language code
LanguageTag string
// Name of the current language
LanguageCurrentName string
// List of other available languages
LanguagePassive []Language
}
// TwoFactorData is a struct that includes parameters for processing two-factor
// authentication. It handles various attributes ranging from welcome messages,
// terms of service, about sections, among others.
type TwoFactorData struct {
// WantWelcome indicates if a welcome message is desired
WantWelcome bool
// WantPolicy indicates if a policy message is required
WantPolicy bool
// WantTos indicates if Terms of Service is mandatory
WantTos bool
// WantAbout indicates if displaying 'About' information is desired
WantAbout bool
// Title is the title of the webpage or context
Title string
// Welcome is the welcome message
Welcome string
// LogoImage is the link of the logo image
LogoImage string
// LogoImageAlt is the alt text of the logo image
LogoImageAlt string
// ApplicationName is the name of the application
ApplicationName string
// Code is the two-factor authentication code
Code string
// Policy is the policy text
Policy string
// PolicyUri is the link to the policy document
PolicyUri string
// Tos is the Terms of Service text
Tos string
// TosUri is the URL to the Terms of Service document
TosUri string
// About holds content related to 'About Us' or 'About the Application'
About string
// AboutUri is the URL to the 'About Us' or 'About the application' page
AboutUri string
// Submit is the label for the submit action
Submit string
// CSRFToken is the token used for Cross-Site Request Forgery protection
CSRFToken string
// LoginChallenge represents the challenge used for login
LoginChallenge string
// User is the User ID or Name
User string
// PostLoginEndpoint is the API endpoint to submit login data
PostLoginEndpoint string
// LanguageTag houses the language tag, e.g., 'en-US'
LanguageTag string
// LanguageCurrentName is the fullname of the current language (e.g., 'English')
LanguageCurrentName string
// LanguagePassive houses a slice of the languages that are passively being used/available
LanguagePassive []Language
}
// LogoutPageData defines the data structure for details related to the logout page.
type LogoutPageData struct {
// WantWelcome is a flag indicating if the welcome message should be displayed or not.
WantWelcome bool
// Title represents the title of the logout page.
Title string
// Welcome holds the welcome message to be displayed, if WantWelcome flag is set to true.
Welcome string
// LogoutMessage carries the logout message.
LogoutMessage string
// AcceptSubmit and RejectSubmit hold messages for submission options upon logout.
// These could be used for multi-step or confirmation based logout procedures.
AcceptSubmit string
RejectSubmit string
// CSRFToken represents the CSRF token for security measures.
CSRFToken string
// LogoutChallenge represents a challenge string for logout.
// It can be used for additional validation on logout requests.
LogoutChallenge string
// PostLogoutEndpoint is the endpoint to which requests are made after logout.
PostLogoutEndpoint string
// LanguageTag refers to the IETF language tag for selected language (e.g. "en-US").
LanguageTag string
// LanguageCurrentName is the human-readable name of the current language (e.g. "English").
LanguageCurrentName string
// LanguagePassive is a slice of passive languages supported by the system.
// These could be offered as alternative language options on the logout page.
LanguagePassive []Language
}
// ConsentPageData defines the data structure for managing user consent information on a web page.
type ConsentPageData struct {
// WantWelcome is a boolean to indicate if a welcome message is needed.
WantWelcome bool
// WantPolicy is a boolean to indicate if a policy is needed.
WantPolicy bool
// WantTos is a boolean to indicate if Terms of Service is required.
WantTos bool
// WantAbout is a boolean to indicate if an "About Us" section is needed.
WantAbout bool
// Title represents the title of the consent page.
Title string
// Welcome represents welcome text message on the page.
Welcome string
// LogoImage represents the URI to logo image on the page.
LogoImage string
// LogoImageAlt is the alternative text for the Logo Image.
LogoImageAlt string
// ConsentMessage is the message shown on the consent page.
ConsentMessage string
// ApplicationName represents the name of the application asking for consent.
ApplicationName string
// Policy represents the text of the policy.
Policy string
// PolicyUri represents the URI to the policy document.
PolicyUri string
// Tos represents the text of the Terms of Service (ToS).
Tos string
// TosUri represents the URI to the Terms of Service (ToS) document.
TosUri string
// About represents the text of the about section.
About string
// AboutUri represents the URI to the about information.
AboutUri string
// Remember is the text related to remember user preferences on the consent page.
Remember string
// AcceptSubmit represents the text on the Accept button.
AcceptSubmit string
// RejectSubmit represents the text on the Reject button.
RejectSubmit string
// CSRFToken is used for CSRF protection.
CSRFToken string
// ConsentChallenge holds the unique consent challenge string from ORY Hydra.
ConsentChallenge string
// PostConsentEndpoint is the endpoint where the browser will be redirected after consent is provided.
PostConsentEndpoint string
// LanguageTag represents the language preference of the client.
LanguageTag string
// LanguageCurrentName represents the current name of the language.
LanguageCurrentName string
// Scopes represents the list of scopes that the app is requesting access to.
Scopes []Scope
// LanguagePassive represents the list of passive languages.
LanguagePassive []Language
}
// NotifyPageData represents page notification data.
type NotifyPageData struct {
// WantWelcome indicates if a welcome message is desired.
WantWelcome bool
// WantPolicy indicates if a policy notification is desired.
WantPolicy bool
// WantTos indicates if terms of service notification is desired.
WantTos bool
// Title represents the title of the notification page.
Title string
// Welcome represents the welcome message on the notification page.
Welcome string
// LogoImage represents the URL of the logo displayed on the notification page.
LogoImage string
// LogoImageAlt represents the alternative text for the logo image.
LogoImageAlt string
// NotifyMessage represents the notification message displayed on the page.
NotifyMessage string
// LanguageTag represents the IETF language tag for the current language.
LanguageTag string
// LanguageCurrentName represents the name of the current language in its language.
LanguageCurrentName string
// LanguagePassive represents a list of other available languages.
LanguagePassive []Language
}
// ApiConfig is a struct that encapsulates configuration and parameters for
// HTTP communication with OAuth2 OpenID-Connect server via OpenAPI. This includes
// configurations for HTTP client, authorization parameters, and request context.
type ApiConfig struct {
// httpClient is a configured HTTP client used to establish connections to the OAuth2 OpenID-connect server.
httpClient *http.Client
// apiClient holds the client information to interact with the OpenAPI.
apiClient *openapi.APIClient
// ctx provides context for HTTP request made against Gin framework.
ctx *gin.Context
// loginRequest is used to store parameters required for OAuth2LoginRequest.
loginRequest *openapi.OAuth2LoginRequest
// consentRequest is used to store parameters required for OAuth2ConsentRequest.
consentRequest *openapi.OAuth2ConsentRequest
// logoutRequest is used to store parameters required for OAuth2LogoutRequest.
logoutRequest *openapi.OAuth2LogoutRequest
// clientId holds client identification which is unique for each application.
clientId *string
// guid is a unique identifier for a specific message or request.
guid string
// csrfToken is used to prevent Cross-Site Request Forgery.
csrfToken string
// clientName holds the name of the client application.
clientName string
// challenge is a unique string used in the authorization process.
challenge string
}
// handleErr handles an error by logging the error details and printing a goroutine dump.
// It sets the "failure" and "message" values in the context, and then calls the notifyGETHandler function.
// If the error is of type *errors2.DetailedError, it logs the error details along with the error message.
// Otherwise, it logs only the error message.
// The function also prints the goroutine dump with the corresponding GUID.
// Finally, it cleans up the session using the sessionCleaner function.
//
// ctx: The Gin context.
// err: The error to handle.
func handleErr(ctx *gin.Context, err error) {
processErrorLogging(ctx, err)
sessionCleaner(ctx)
ctx.Set("failure", true)
ctx.Set("message", err)
notifyGETHandler(ctx)
}
// processErrorLogging logs the error details and prints a goroutine dump.
// It takes the Gin context and the error as inputs.
// It retrieves the GUID from the context and logs the error using the logError function.
// It then creates a buffer and uses the runtime.Stack function to fill the buffer with a goroutine dump.
// Finally, it prints the goroutine dump along with the GUID to the console.
//
// ctx: The Gin context.
// err: The error to log.
// Usage example:
//
// handleErr(ctx, err)
// sessionCleaner(ctx)
// ctx.Set("failure", true)
// ctx.Set("message", err)
// notifyGETHandler(ctx)
//
// See logError, global.CtxGUIDKey, and runtime.Stack for additional information.
func processErrorLogging(ctx *gin.Context, err error) {
guid := ctx.GetString(global.CtxGUIDKey)
logError(ctx, err)
if config.LoadableConfig.Server.Log.Level.Level() == global.LogLevelDebug && config.EnvConfig.DevMode {
buf := make([]byte, 1<<20)
stackLen := runtime.Stack(buf, false)
fmt.Printf("=== guid=%s\n*** goroutine dump...\n%s\n*** end\n", guid, buf[:stackLen])
}
}
// logError logs the error details along with the corresponding GUID, client IP, and error message.
// If the error is of type *errors2.DetailedError, it logs the error details using logging.DefaultErrLogger.Log method.
// Otherwise, it logs only the error message.
//
// ctx: The Gin context.
// err: The error to log.
func logError(ctx *gin.Context, err error) {
var detailedError *errors2.DetailedError
guid := ctx.GetString(global.CtxGUIDKey)
if errors.As(err, &detailedError) {
level.Error(logging.DefaultErrLogger).Log(
global.LogKeyGUID, guid,
global.LogKeyError, (*detailedError).Error(),
global.LogKeyErrorDetails, (*detailedError).GetDetails(),
global.LogKeyClientIP, ctx.Request.RemoteAddr,
)
} else {
level.Error(logging.DefaultErrLogger).Log(
global.LogKeyGUID, guid,
global.LogKeyError, err,
global.LogKeyClientIP, ctx.Request.RemoteAddr,
)
}
}
// notifyGETHandler handles the GET request for the notification page.
// It sets the HTTP status code, status title, and notification message based on the context.
// It also prepares the data for rendering the notify.html template and executes the HTML rendering.
func notifyGETHandler(ctx *gin.Context) {
var (
found bool
msg string
value any
httpStatusCode = http.StatusOK
)
statusTitle := getLocalized(ctx, "Information")
if value, found = ctx.Get("failure"); found {
if value.(bool) {
httpStatusCode = http.StatusBadRequest
statusTitle = getLocalized(ctx, "Bad Request")
}
}
if value, found = ctx.Get("message"); found {
msg = getLocalized(ctx, "An error occurred:") + " " + value.(error).Error()
} else {
msg = getLocalized(ctx, ctx.Query("message"))
}
// Fallback for non-localized messages
if msg == "" {
msg = ctx.Query("message")
}
session := sessions.Default(ctx)
cookieValue := session.Get(global.CookieLang)
languageCurrentTag := language.MustParse(cookieValue.(string))
languageCurrentName := cases.Title(languageCurrentTag, cases.NoLower).String(display.Self.Name(languageCurrentTag))
languagePassive := createLanguagePassive(ctx, viper.GetString("notify_page"), config.DefaultLanguageTags, languageCurrentName)
notifyData := NotifyPageData{
Title: statusTitle,
WantWelcome: func() bool {
if viper.GetString("notify_page_welcome") != "" {
return true
}
return false
}(),
Welcome: viper.GetString("notify_page_welcome"),
LogoImage: viper.GetString("default_logo_image"),
LogoImageAlt: viper.GetString("notify_page_logo_image_alt"),
NotifyMessage: msg,
LanguageTag: session.Get(global.CookieLang).(string),
LanguageCurrentName: languageCurrentName,
LanguagePassive: languagePassive,
WantTos: false,
WantPolicy: false,
}
ctx.HTML(httpStatusCode, "notify.html", notifyData)
}
// getLocalized is a function that returns the localized message based on the message ID and the context provided.
// If the localization fails, an error is logged.
func getLocalized(ctx *gin.Context, messageID string) string {
localizer := ctx.MustGet(global.CtxLocalizedKey).(*i18n.Localizer)
localizeConfig := i18n.LocalizeConfig{
MessageID: messageID,
}
localization, err := localizer.Localize(&localizeConfig)
if err != nil {
level.Error(logging.DefaultErrLogger).Log(
global.LogKeyGUID, ctx.GetString(global.CtxGUIDKey),
"message_id", messageID, global.LogKeyError, err.Error(),
)
}
return localization
}
// handleHydraErr handles an error by checking the status code of the http response.
// If the status code is StatusNotFound, it calls the handleErr function with errors2.ErrUnknownJSON as the error.
// If the status code is StatusGone, it calls the handleErr function with errors2.ErrHTTPRequestGone as the error.
// Otherwise, it calls the handleErr function with the original error.
// If the http response is nil, it calls the handleErr function with the original error.
//
// ctx: The Gin context.
// err: The error to handle.
// httpResponse: The http response object.
// handleErr: The function that handles an error.
// errors2.ErrUnknownJSON: The error representing an unknown JSON response.
// errors2.ErrHTTPRequestGone: The error representing a gone http request.
func handleHydraErr(ctx *gin.Context, err error, httpResponse *http.Response) {
if httpResponse != nil {
switch httpResponse.StatusCode {
case http.StatusNotFound:
handleErr(ctx, errors2.ErrUnknownJSON)
case http.StatusGone:
handleErr(ctx, errors2.ErrHTTPRequestGone)
default:
handleErr(ctx, err)
}
} else {
handleErr(ctx, err)
}
}
// setLanguageDetails determines the language details based on the provided langFromURL and langFromCookie parameters.
// It returns the selected lang, needCookie, and needRedirect values.
// The algorithm for determining the values is as follows:
//
// 1. If there is no language from the URL and no cookie is set, set needCookie and needRedirect to true.
// 2. If there is no language from the URL but a cookie is set, set lang to langFromCookie and needRedirect to true.
// 3. If there is a language from the URL and no cookie, set lang to langFromURL and needCookie to true.
// 4. If there is a language from both the URL and the cookie, and they differ, set lang to langFromURL and needCookie to true.
//
// The function returns lang, needCookie, and needRedirect.
//
//goland:noinspection GoDfaConstantCondition
func setLanguageDetails(langFromURL string, langFromCookie string) (lang string, needCookie bool, needRedirect bool) {
if langFromURL == "" && langFromCookie == "" {
// 1. No language from URL and no cookie is set
needCookie = true
needRedirect = true
} else if langFromURL == "" && langFromCookie != "" {
// 2. No language from URL, but a cookie is set
lang = langFromCookie
needRedirect = true
} else if langFromURL != "" && langFromCookie == "" {
// 3. Language from URL and no cookie
lang = langFromURL
needCookie = true
} else if langFromURL != "" && langFromCookie != "" {
if langFromURL != langFromCookie {
// 4. Langauge given from URL and cookie, but both differ
needCookie = true
}
lang = langFromURL
}
return lang, needCookie, needRedirect
}
// withLanguageMiddleware is a middleware function that handles the language setup for the application.
// It tries to get the language tag from the URL and the cookie.
// It sets the language details and creates a localizer based on the selected language.
// It also handles CSRF token and localization in the context.
// If the language is not found in the catalog, it aborts the request with a "Language Not Found" error.
// If the language needs to be saved in a cookie or redirection is required, it does so accordingly.
// Finally, it calls the next handler in the chain.
func withLanguageMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
var (
langFromURL string
langFromCookie string
)
guid := ctx.GetString(global.CtxGUIDKey)
// Try to get language tag from URL
langFromURL = ctx.Param("languageTag")
// Try to get language tag from cookie
session := sessions.Default(ctx)
cookieValue := session.Get(global.CookieLang)
if cookieValue != nil {
langFromCookie, _ = cookieValue.(string)
}
lang, needCookie, needRedirect := setLanguageDetails(langFromURL, langFromCookie)
accept := ctx.GetHeader("Accept-Language")
tag, _ := language.MatchStrings(config.Matcher, lang, accept)
baseName, _ := tag.Base()
util.DebugModule(
global.DbgHydra,
global.LogKeyGUID, guid,
"accept", accept,
"language", lang,
"language_tag", fmt.Sprintf("%v", baseName.String()),
)
// Language not found in catalog
if lang != "" && lang != baseName.String() {
ctx.AbortWithError(http.StatusNotFound, errors2.ErrLanguageNotFound)
return
}
localizer := i18n.NewLocalizer(LangBundle, lang, accept)
if needCookie {
session.Set(global.CookieLang, baseName.String())
session.Save()
}
ctx.Set(global.CtxCSRFTokenKey, nosurf.Token(ctx.Request))
ctx.Set(global.CtxLocalizedKey, localizer)
if needRedirect {
ctx.Redirect(
http.StatusFound,
ctx.Request.URL.Path+"/"+baseName.String()+"?"+ctx.Request.URL.RawQuery,
)
return
}
ctx.Next()
}
}
// createHttpClient creates an HTTP client with a custom configuration.
// The client uses an http.Transport with a custom *tls.Config, which allows skipping TLS verification
// based on the value of the "http_client_skip_tls_verify" configuration.
// The client also has a Timeout of 30 seconds.
// Returns the created http.Client.
func createHttpClient() *http.Client {
return &http.Client{
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: config.LoadableConfig.Server.TLS.HTTPClientSkipVerify}},
Timeout: 30 * time.Second,
}
}
// createConfiguration returns a new instance of the openapi.Configuration struct with the provided httpClient and server configuration.
// The httpClient parameter is used as the underlying HTTP client for API calls made by the openapi.client.
// The server configuration is read from the "hydra_admin_uri" configuration value using viper.GetString() function.
func createConfiguration(httpClient *http.Client) *openapi.Configuration {
return &openapi.Configuration{
HTTPClient: httpClient,
Servers: []openapi.ServerConfiguration{{URL: config.LoadableConfig.Server.HydraAdminUrl}},
}
}
// createUserdata creates a map containing user data from a session based on the given keys.
// It returns a map[string]any. The function iterates through the keys and retrieves the corresponding values from the session.
// If the value is not nil, it is added to the userData map with the key as the key in the session.
//
// Params:
// - session: the session to retrieve data from
// - keys: the keys to retrieve data for
//
// Returns:
// - userData: a map containing the user data
func createUserdata(session sessions.Session, keys ...string) map[string]any {
userData := make(map[string]any, len(keys))
for _, key := range keys {
value := session.Get(key)
if value != nil {
userData[key] = value
}
}
return userData
}
// createLanguagePassive is a function that takes a gin.Context and a slice of language.Tags as input,
// along with the currentName string. It returns a slice of Language structs. The function iterates over
// the languageTags slice and creates a Language struct for each tag, except the one with the currentName.
// The Language struct has two fields: LanguageLink and LanguageName.
// The function appends each created Language struct to the languagePassive slice, and finally returns
// the languagePassive slice.
func createLanguagePassive(ctx *gin.Context, destPage string, languageTags []language.Tag, currentName string) []Language {
var languagePassive []Language
for _, languageTag := range languageTags {
languageName := cases.Title(languageTag, cases.NoLower).String(display.Self.Name(languageTag))
if languageName != currentName {
baseName, _ := languageTag.Base()
languagePassive = append(
languagePassive,
Language{
LanguageLink: destPage + "/" + baseName.String() + "?" + ctx.Request.URL.RawQuery,
LanguageName: languageName,
},
)
}
}
return languagePassive
}
// initialize sets up the `ApiConfig` object by initializing the HTTP client, GUID, and API client.
// Must be called before using any other methods on `ApiConfig`.
//
// Example usage:
//
// apiConfig := &ApiConfig{ctx: ctx}
// apiConfig.initialize()
//
// // Use the initialized `ApiConfig` object
// apiConfig.handleLogin(apiConfig.loginRequest.GetSkip())
//
// Dependencies:
// - `createHttpClient` function
// - `createConfiguration` function
//
// Note: This method assumes that the `ApiConfig` object is properly initialized with the `ctx` field set.
func (a *ApiConfig) initialize() {
a.httpClient = createHttpClient()
a.guid = a.ctx.GetString(global.CtxGUIDKey)
configuration := createConfiguration(a.httpClient)
a.apiClient = openapi.NewAPIClient(configuration)
}
// handleLogin handles the login process based on the value of `skip`.
//
// If `skip` is true, it calls the `handleLoginSkip` method.
// If `skip` is false, it calls the `handleLoginNoSkip` method.
//
// Example usage:
//
// apiConfig := &ApiConfig{ctx: ctx}
// apiConfig.initialize()
// apiConfig.handleLogin(apiConfig.loginRequest.GetSkip())
//
// Dependencies:
// - `handleLoginSkip` method
// - `handleLoginNoSkip` method
func (a *ApiConfig) handleLogin(skip bool) {
util.DebugModule(global.DbgHydra, global.LogKeyGUID, a.guid, global.LogKeyMsg, fmt.Sprintf("%s is %v", global.LogKeyLoginSkip, skip))
if skip {
a.handleLoginSkip()
} else {
a.handleLoginNoSkip()
}
}
// handleLoginSkip processes the login request when skip is true.
func (a *ApiConfig) handleLoginSkip() {
var (
err error
acceptRequest *openapi.OAuth2RedirectTo
httpResponse *http.Response
claims map[string]any
)
util.DebugModule(global.DbgHydra, global.LogKeyGUID, a.guid, global.LogKeyMsg, fmt.Sprintf("%s is %v", global.LogKeyLoginSkip, true))
oauth2Client := a.loginRequest.GetClient()
auth := &Authentication{
HTTPClientContext: a.ctx.Copy(),
NoAuth: true,
Protocol: config.NewProtocol(global.ProtoOryHydra),
}
auth.withDefaults(a.ctx).withClientInfo(a.ctx).withLocalInfo(a.ctx).withUserAgent(a.ctx).withXSSL(a.ctx)
auth.Username = a.loginRequest.GetSubject()
auth.UsernameOrig = a.loginRequest.GetSubject()
if err := auth.setStatusCodes(global.ServOryHydra); err != nil {
handleErr(a.ctx, err)
return
}
if authStatus := auth.handlePassword(a.ctx); authStatus == global.AuthResultOK {
if config.LoadableConfig.Oauth2 != nil {
_, claims = auth.getOauth2SubjectAndClaims(oauth2Client)
}
} else {
auth.ClientIP = a.ctx.GetString(global.CtxClientIPKey)
auth.updateBruteForceBucketsCounter()
a.ctx.AbortWithError(http.StatusInternalServerError, errors2.ErrUnknownCause)
return
}
acceptLoginRequest := a.apiClient.OAuth2API.AcceptOAuth2LoginRequest(a.ctx).AcceptOAuth2LoginRequest(
openapi.AcceptOAuth2LoginRequest{
Subject: a.loginRequest.GetSubject(),
Context: claims,
})
acceptRequest, httpResponse, err = acceptLoginRequest.LoginChallenge(a.challenge).Execute()
if err != nil {
handleHydraErr(a.ctx, err, httpResponse)
return
}
a.ctx.Redirect(http.StatusFound, acceptRequest.GetRedirectTo())
a.logInfoLoginSkip(acceptRequest.GetRedirectTo())
}
// handleLoginNoSkip handles the login process when skip is false.
//
// It retrieves the necessary information from the OAuth2 client, such as image URI, policy URI,
// terms of service URI, and client URI. It also retrieves the application name from the client and
// creates the user data and language information.
//
// If the pre2FA flag is set and the user has already authenticated, it handles the TOTP (Time-based
// One-Time Password) request and returns the HTML response to the client with two factor authentication data.
//
// If the _error query parameter is not empty, it sets the haveError flag and retrieves the error message.
//
// Finally, it constructs the login page data and returns the HTML response to the client with the login data.
//
// Dependencies:
// - getLocalized function
// - createLanguagePassive function
// - TwoFactorData struct
// - LoginPageData struct
func (a *ApiConfig) handleLoginNoSkip() {
var (
wantAbout bool
wantPolicy bool
wantTos bool
haveError bool
policyUri string
tosUri string
clientUri string
imageUri string
errorMessage string
)
util.DebugModule(global.DbgHydra, global.LogKeyGUID, a.guid, global.LogKeyMsg, fmt.Sprintf("%s is %v", global.LogKeyLoginSkip, false))
oauth2Client := a.loginRequest.GetClient()
imageUri = oauth2Client.GetLogoUri()
if imageUri == "" {
imageUri = viper.GetString("default_logo_image")
}
if policyUri = oauth2Client.GetPolicyUri(); policyUri != "" {
wantPolicy = true
}
if tosUri = oauth2Client.GetTosUri(); tosUri != "" {
wantTos = true
}
if clientUri = oauth2Client.GetClientUri(); clientUri != "" {
wantAbout = true
}
applicationName := oauth2Client.GetClientName()
session := sessions.Default(a.ctx)
cookieValue := session.Get(global.CookieLang)
languageCurrentTag := language.MustParse(cookieValue.(string))
languageCurrentName := cases.Title(languageCurrentTag, cases.NoLower).String(display.Self.Name(languageCurrentTag))
languagePassive := createLanguagePassive(a.ctx, viper.GetString("login_page"), config.DefaultLanguageTags, languageCurrentName)
userData := createUserdata(session, global.CookieUsername, global.CookieAuthResult)
// Handle TOTP request
if authResult, found := userData[global.CookieAuthResult]; found {
if authResult != global.AuthResultUnset {
twoFactorData := &TwoFactorData{
Title: getLocalized(a.ctx, "Login"),
WantWelcome: func() bool {
if viper.GetString("login_page_welcome") != "" {
return true
}
return false
}(),
Welcome: viper.GetString("login_page_welcome"),
ApplicationName: applicationName,
WantAbout: wantAbout,
About: getLocalized(a.ctx, "Get further information about this application..."),
AboutUri: clientUri,
LogoImage: imageUri,
LogoImageAlt: viper.GetString("login_page_logo_image_alt"),
WantPolicy: wantPolicy,
Code: getLocalized(a.ctx, "OTP-Code"),
Policy: getLocalized(a.ctx, "Privacy policy"),
PolicyUri: policyUri,
WantTos: wantTos,
Tos: getLocalized(a.ctx, "Terms of service"),
TosUri: tosUri,
Submit: getLocalized(a.ctx, "Submit"),
PostLoginEndpoint: viper.GetString("login_page"),
LanguageTag: session.Get(global.CookieLang).(string),
LanguageCurrentName: languageCurrentName,
LanguagePassive: languagePassive,
CSRFToken: a.csrfToken,
LoginChallenge: a.challenge,
}
a.ctx.HTML(http.StatusOK, "totp.html", twoFactorData)