diff --git a/active.de.toml b/active.de.toml index cf0df15..9dc347c 100644 --- a/active.de.toml +++ b/active.de.toml @@ -70,3 +70,7 @@ other = "Deine Nutzerprofilinformationen inklusive Name, Geburtsdatum und Sprach [TitleRequestConsent] hash = "sha1-14984a9e08cda5390677458a98408baa955f9f8b" other = "Anwendung erbittet deine Zustimmung" + +[WrongOrLockedUserOrInvalidPassword] +hash = "sha1-87e0a0ac67c6c3a06bed184e10b22aae4d075b64" +other = "Du hast einen ungültigen Nutzernamen oder ein ungültiges Passwort eingegeben oder dein Benutzerkonto wurde gesperrt." diff --git a/active.en.toml b/active.en.toml index 1a45941..41c695c 100644 --- a/active.en.toml +++ b/active.en.toml @@ -15,6 +15,7 @@ Scope-offline_access-Description = "Keep access to your information until you re Scope-openid-Description = "Request information about your identity." Scope-profile-Description = "Access your user profile information including your name, birth date and locale." TitleRequestConsent = "Application requests your consent" +WrongOrLockedUserOrInvalidPassword = "You entered an invalid username or password or your account has been locked." [LogoutLabel] description = "A label on a logout button or link" diff --git a/cmd/idp/main.go b/cmd/idp/main.go index b3585b2..81826aa 100644 --- a/cmd/idp/main.go +++ b/cmd/idp/main.go @@ -85,6 +85,11 @@ func main() { clientTransport := client.New(adminURL.Host, adminURL.Path, []string{adminURL.Scheme}) adminClient := hydra.New(clientTransport, nil) + ctx, err = services.InitDatabase(ctx, services.NewDatabaseParams(config.MustString("db.dsn"))) + if err != nil { + logger.Fatalf("error initializing the database connection: %v", err) + } + handlerContext := context.WithValue(ctx, handlers.CtxAdminClient, adminClient.Admin) loginHandler, err := handlers.NewLoginHandler(handlerContext, logger) if err != nil { @@ -94,7 +99,7 @@ func main() { if err != nil { logger.Fatalf("error initializing consent handler: %v", err) } - logoutHandler := handlers.NewLogoutHandler(logger, handlerContext) + logoutHandler := handlers.NewLogoutHandler(handlerContext, logger) logoutSuccessHandler := handlers.NewLogoutSuccessHandler() errorHandler := handlers.NewErrorHandler() diff --git a/go.mod b/go.mod index 261d84d..2bc175b 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,11 @@ require ( github.com/go-openapi/runtime v0.19.22 github.com/go-playground/form/v4 v4.1.1 github.com/go-playground/validator/v10 v10.4.1 + github.com/go-sql-driver/mysql v1.5.0 github.com/golang/protobuf v1.4.0 // indirect github.com/gorilla/csrf v1.7.0 github.com/gorilla/sessions v1.2.1 + github.com/jmoiron/sqlx v1.2.0 github.com/knadh/koanf v0.14.0 github.com/lestrrat-go/jwx v1.0.6 github.com/nicksnyder/go-i18n/v2 v2.1.1 @@ -20,6 +22,7 @@ require ( golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 // indirect golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d - golang.org/x/text v0.3.3 + golang.org/x/text v0.3.4 google.golang.org/appengine v1.6.5 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 1be64c9..b9c7328 100644 --- a/go.sum +++ b/go.sum @@ -102,6 +102,9 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= @@ -155,6 +158,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= @@ -180,6 +185,8 @@ github.com/lestrrat-go/jwx v1.0.6 h1:0absmJ/XlsxNkXr9syeIHjCJnu3rZa+DKzdCI6QfYgU github.com/lestrrat-go/jwx v1.0.6/go.mod h1:NNxs6i86gQDGEqgIszN/pkJihMqzYrXMIJt2Yhxhkvs= github.com/lestrrat-go/pdebug v0.0.0-20200204225717-4d6bd78da58d h1:aEZT3f1GGg5RIlHMAy4/4fe4ciOi3SCwYoaURphcB4k= github.com/lestrrat-go/pdebug v0.0.0-20200204225717-4d6bd78da58d/go.mod h1:B06CSso/AWxiPejj+fheUINGeBKeeEZNt8w+EoU7+L8= +github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -188,11 +195,14 @@ github.com/mailru/easyjson v0.7.1 h1:mdxE1MF9o53iCb2Ghj1VfWvh7ZOwHpnVG/xwXrV90U8 github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg= github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/nicksnyder/go-i18n v1.10.1 h1:isfg77E/aCD7+0lD/D00ebR2MV5vgeQ276WYyDaCRQc= github.com/nicksnyder/go-i18n/v2 v2.1.1 h1:ATCOanRDlrfKVB4WHAdJnLEqZtDmKYsweqsOUYflnBU= github.com/nicksnyder/go-i18n/v2 v2.1.1/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= @@ -297,6 +307,8 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -332,6 +344,8 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= diff --git a/idp/handlers/consent.go b/idp/handlers/consent.go index 9dabe29..0cccf14 100644 --- a/idp/handlers/consent.go +++ b/idp/handlers/consent.go @@ -2,12 +2,15 @@ package handlers import ( "context" + "database/sql" "fmt" "html/template" "net/http" + "strings" "time" "github.com/go-playground/form/v4" + "github.com/go-sql-driver/mysql" "github.com/gorilla/csrf" "github.com/lestrrat-go/jwx/jwt/openid" "github.com/nicksnyder/go-i18n/v2/i18n" @@ -16,12 +19,14 @@ import ( log "github.com/sirupsen/logrus" commonServices "git.cacert.org/oidc_login/common/services" + "git.cacert.org/oidc_login/idp/services" ) type consentHandler struct { adminClient *admin.Client bundle *i18n.Bundle consentTemplate *template.Template + context context.Context logger *log.Logger messageCatalog *commonServices.MessageCatalog } @@ -30,6 +35,31 @@ type ConsentInformation struct { ConsentChecked bool `form:"consent"` } +type UserInfo struct { + Email string `db:"email"` + EmailVerified bool `db:"verified"` + GivenName string `db:"fname"` + MiddleName string `db:"mname"` + FamilyName string `db:"lname"` + BirthDate mysql.NullTime `db:"dob"` + Language string `db:"language"` + Modified mysql.NullTime `db:"modified"` +} + +func (i *UserInfo) GetFullName() string { + nameParts := make([]string, 0) + if len(i.GivenName) > 0 { + nameParts = append(nameParts, i.GivenName) + } + if len(i.MiddleName) > 0 { + nameParts = append(nameParts, i.MiddleName) + } + if len(i.FamilyName) > 0 { + nameParts = append(nameParts, i.FamilyName) + } + return strings.Join(nameParts, " ") +} + func (h *consentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { challenge := r.URL.Query().Get("consent_challenge") h.logger.Debugf("received consent challenge %s", challenge) @@ -47,26 +77,7 @@ func (h *consentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - trans := h.messageCatalog.LookupMessage - - // render consent form - client := consentData.GetPayload().Client - err = h.consentTemplate.Lookup("base").Execute(w, map[string]interface{}{ - "Title": trans("TitleRequestConsent", nil, localizer), - csrf.TemplateTag: csrf.TemplateField(r), - "errors": map[string]string{}, - "client": client, - "requestedScope": h.mapRequestedScope(consentData.GetPayload().RequestedScope, localizer), - "LabelSubmit": trans("LabelSubmit", nil, localizer), - "LabelConsent": trans("LabelConsent", nil, localizer), - "IntroMoreInformation": template.HTML(trans("IntroConsentMoreInformation", map[string]interface{}{ - "client": client.ClientName, - "clientLink": client.ClientURI, - }, localizer)), - "IntroConsentRequested": template.HTML(trans("IntroConsentRequested", map[string]interface{}{ - "client": client.ClientName, - }, localizer)), - }) + h.renderConsentForm(w, r, consentData, err, localizer) break case http.MethodPost: var consentInfo ConsentInformation @@ -82,46 +93,79 @@ func (h *consentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if consentInfo.ConsentChecked { idTokenData := make(map[string]interface{}, 0) - for _, scope := range consentData.GetPayload().RequestedScope { - switch scope { - case "email": - idTokenData[openid.EmailKey] = "john@theripper.mil" - idTokenData[openid.EmailVerifiedKey] = true - break - case "profile": - idTokenData[openid.GivenNameKey] = "John" - idTokenData[openid.FamilyNameKey] = "The ripper" - idTokenData[openid.MiddleNameKey] = "" - idTokenData[openid.NameKey] = "John the Ripper" - idTokenData[openid.BirthdateKey] = "1970-01-01" - idTokenData[openid.ZoneinfoKey] = "Europe/London" - idTokenData[openid.LocaleKey] = "en_UK" - idTokenData["https://cacert.localhost/groups"] = []string{"admin", "user"} - break - } - } + db := services.GetDb(h.context) - sessionData := &models.ConsentRequestSession{ - AccessToken: nil, - IDToken: idTokenData, - } - consentRequest, err := h.adminClient.AcceptConsentRequest( - admin.NewAcceptConsentRequestParams().WithConsentChallenge(challenge).WithBody( - &models.AcceptConsentRequest{ - GrantAccessTokenAudience: nil, - GrantScope: consentData.GetPayload().RequestedScope, - HandledAt: models.NullTime(time.Now()), - Remember: true, - RememberFor: 86400, - Session: sessionData, - }).WithTimeout(time.Second * 10)) + stmt, err := db.PreparexContext( + r.Context(), + `SELECT email, verified, fname, mname, lname, dob, language, modified +FROM users +WHERE id = ? + AND LOCKED = 0`, + ) if err != nil { - h.logger.Error(err) + h.logger.Errorf("error preparing user information SQL: %v", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - w.Header().Add("Location", *consentRequest.GetPayload().RedirectTo) - w.WriteHeader(http.StatusFound) + defer func() { _ = stmt.Close() }() + + userInfo := &UserInfo{} + + err = stmt.QueryRowxContext(r.Context(), consentData.GetPayload().Subject).StructScan(userInfo) + switch { + case err == sql.ErrNoRows: + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + case err != nil: + h.logger.Errorf("error performing user information SQL: %v", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + default: + for _, scope := range consentData.GetPayload().RequestedScope { + switch scope { + case "email": + idTokenData[openid.EmailKey] = userInfo.Email + idTokenData[openid.EmailVerifiedKey] = userInfo.EmailVerified + break + case "profile": + idTokenData[openid.GivenNameKey] = userInfo.GivenName + idTokenData[openid.FamilyNameKey] = userInfo.FamilyName + idTokenData[openid.MiddleNameKey] = userInfo.MiddleName + idTokenData[openid.NameKey] = userInfo.GetFullName() + if userInfo.BirthDate.Valid { + idTokenData[openid.BirthdateKey] = userInfo.BirthDate.Time.Format("2006-01-02") + } + idTokenData[openid.LocaleKey] = userInfo.Language + idTokenData["https://cacert.localhost/groups"] = []string{"admin", "user"} + if userInfo.Modified.Valid { + idTokenData[openid.UpdatedAtKey] = userInfo.Modified.Time.Unix() + } + break + } + } + sessionData := &models.ConsentRequestSession{ + AccessToken: nil, + IDToken: idTokenData, + } + consentRequest, err := h.adminClient.AcceptConsentRequest( + admin.NewAcceptConsentRequestParams().WithConsentChallenge(challenge).WithBody( + &models.AcceptConsentRequest{ + GrantAccessTokenAudience: nil, + GrantScope: consentData.GetPayload().RequestedScope, + HandledAt: models.NullTime(time.Now()), + Remember: true, + RememberFor: 86400, + Session: sessionData, + }).WithTimeout(time.Second * 10)) + if err != nil { + h.logger.Error(err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + w.Header().Add("Location", *consentRequest.GetPayload().RedirectTo) + w.WriteHeader(http.StatusFound) + return + } } else { consentRequest, err := h.adminClient.RejectConsentRequest( admin.NewRejectConsentRequestParams().WithConsentChallenge(challenge).WithBody( @@ -137,6 +181,34 @@ func (h *consentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } +func (h *consentHandler) renderConsentForm(w http.ResponseWriter, r *http.Request, consentData *admin.GetConsentRequestOK, err error, localizer *i18n.Localizer) { + trans := func(id string, values ...map[string]interface{}) string { + if len(values) > 0 { + return h.messageCatalog.LookupMessage(id, values[0], localizer) + } + return h.messageCatalog.LookupMessage(id, nil, localizer) + } + + // render consent form + client := consentData.GetPayload().Client + err = h.consentTemplate.Lookup("base").Execute(w, map[string]interface{}{ + "Title": trans("TitleRequestConsent"), + csrf.TemplateTag: csrf.TemplateField(r), + "errors": map[string]string{}, + "client": client, + "requestedScope": h.mapRequestedScope(consentData.GetPayload().RequestedScope, localizer), + "LabelSubmit": trans("LabelSubmit"), + "LabelConsent": trans("LabelConsent"), + "IntroMoreInformation": template.HTML(trans("IntroConsentMoreInformation", map[string]interface{}{ + "client": client.ClientName, + "clientLink": client.ClientURI, + })), + "IntroConsentRequested": template.HTML(trans("IntroConsentRequested", map[string]interface{}{ + "client": client.ClientName, + })), + }) +} + type scopeWithLabel struct { Name string Label string @@ -162,6 +234,7 @@ func NewConsentHandler(ctx context.Context, logger *log.Logger) (*consentHandler adminClient: ctx.Value(CtxAdminClient).(*admin.Client), bundle: commonServices.GetI18nBundle(ctx), consentTemplate: consentTemplate, + context: ctx, logger: logger, messageCatalog: commonServices.GetMessageCatalog(ctx), }, nil diff --git a/idp/handlers/login.go b/idp/handlers/login.go index 5ba6568..24b584b 100644 --- a/idp/handlers/login.go +++ b/idp/handlers/login.go @@ -2,6 +2,9 @@ package handlers import ( "context" + "crypto/sha1" + "database/sql" + "encoding/hex" "html/template" "net/http" "time" @@ -16,11 +19,13 @@ import ( log "github.com/sirupsen/logrus" commonServices "git.cacert.org/oidc_login/common/services" + "git.cacert.org/oidc_login/idp/services" ) type loginHandler struct { adminClient *admin.Client bundle *i18n.Bundle + context context.Context logger *log.Logger loginTemplate *template.Template messageCatalog *commonServices.MessageCatalog @@ -47,24 +52,15 @@ func (h *loginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { var err error challenge := r.URL.Query().Get("login_challenge") h.logger.Debugf("received login challenge %s\n", challenge) + accept := r.Header.Get("Accept-Language") + localizer := i18n.NewLocalizer(h.bundle, accept) + validate := validator.New() switch r.Method { case http.MethodGet: // render login form - err = h.loginTemplate.Lookup("base").Execute(w, map[string]interface{}{ - "Title": "Title", - csrf.TemplateTag: csrf.TemplateField(r), - "LabelEmail": "Email", - "LabelPassword": "Password", - "LabelLogin": "Login", - "errors": map[string]string{}, - }) - if err != nil { - h.logger.Error(err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } + h.renderLoginForm(w, r, map[string]string{}, &LoginInformation{}, localizer) break case http.MethodPost: var loginInfo LoginInformation @@ -84,42 +80,66 @@ func (h *loginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { accept := r.Header.Get("Accept-Language") errors[err.Field()] = h.messageCatalog.LookupErrorMessage(err.Tag(), err.Field(), err.Value(), i18n.NewLocalizer(h.bundle, accept)) } + h.renderLoginForm(w, r, errors, &loginInfo, localizer) + return + } - err = h.loginTemplate.Lookup("base").Execute(w, map[string]interface{}{ - "Title": "Title", - csrf.TemplateTag: csrf.TemplateField(r), - "LabelEmail": "Email", - "LabelPassword": "Password", - "LabelLogin": "Login", - "Email": loginInfo.Email, - "errors": errors, - }) + db := services.GetDb(h.context) + + stmt, err := db.PrepareContext( + r.Context(), + `SELECT id +FROM users +WHERE email = ? + AND password = ? + AND locked = 0`, + ) + if err != nil { + h.logger.Errorf("error preparing login SQL: %v", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + defer func() { _ = stmt.Close() }() + + // FIXME: replace with a real password hash algorithm + passwordHash := sha1.Sum([]byte(loginInfo.Password)) + password := hex.EncodeToString(passwordHash[:]) + // FIXME: introduce a real opaque identifier (i.e. a UUID) + var userId string + // GET user data + err = stmt.QueryRowContext(r.Context(), loginInfo.Email, password).Scan(&userId) + switch { + case err == sql.ErrNoRows: + errors := map[string]string{ + "Form": h.messageCatalog.LookupMessage( + "WrongOrLockedUserOrInvalidPassword", + nil, + localizer, + ), + } + h.renderLoginForm(w, r, errors, &loginInfo, localizer) + return + case err != nil: + h.logger.Errorf("error performing login SQL: %v", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + default: + // finish login and redirect to target + loginRequest, err := h.adminClient.AcceptLoginRequest( + admin.NewAcceptLoginRequestParams().WithLoginChallenge(challenge).WithBody(&models.AcceptLoginRequest{ + Acr: string(Password), + Remember: true, + RememberFor: 0, + Subject: &userId, + }).WithTimeout(time.Second * 10)) if err != nil { - h.logger.Error(err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + h.logger.Errorf("error getting login request: %#v", err) + http.Error(w, err.Error(), err.(*runtime.APIError).Code) return } - return + w.Header().Add("Location", *loginRequest.GetPayload().RedirectTo) + w.WriteHeader(http.StatusFound) } - - // GET user data - // finish login and redirect to target - // TODO: get or generate a user id - subject := "a-user-with-an-id" - loginRequest, err := h.adminClient.AcceptLoginRequest( - admin.NewAcceptLoginRequestParams().WithLoginChallenge(challenge).WithBody(&models.AcceptLoginRequest{ - Acr: string(NoCredentials), - Remember: true, - RememberFor: 0, - Subject: &subject, - }).WithTimeout(time.Second * 10)) - if err != nil { - h.logger.Errorf("error getting login request: %#v", err) - http.Error(w, err.Error(), err.(*runtime.APIError).Code) - return - } - w.Header().Add("Location", *loginRequest.GetPayload().RedirectTo) - w.WriteHeader(http.StatusFound) break default: http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) @@ -127,6 +147,27 @@ func (h *loginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } +func (h *loginHandler) renderLoginForm(w http.ResponseWriter, r *http.Request, errors map[string]string, info *LoginInformation, localizer *i18n.Localizer) { + trans := func(label string) string { + return h.messageCatalog.LookupMessage(label, nil, localizer) + } + + err := h.loginTemplate.Lookup("base").Execute(w, map[string]interface{}{ + "Title": trans("LoginTitle"), + csrf.TemplateTag: csrf.TemplateField(r), + "LabelEmail": trans("LabelEmail"), + "LabelPassword": trans("LabelPassword"), + "LabelLogin": trans("LabelLogin"), + "Email": info.Email, + "errors": errors, + }) + if err != nil { + h.logger.Error(err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } +} + func NewLoginHandler(ctx context.Context, logger *log.Logger) (*loginHandler, error) { loginTemplate, err := template.ParseFiles( "templates/idp/base.gohtml", "templates/idp/login.gohtml") @@ -136,6 +177,7 @@ func NewLoginHandler(ctx context.Context, logger *log.Logger) (*loginHandler, er return &loginHandler{ adminClient: ctx.Value(CtxAdminClient).(*admin.Client), bundle: commonServices.GetI18nBundle(ctx), + context: ctx, logger: logger, loginTemplate: loginTemplate, messageCatalog: commonServices.GetMessageCatalog(ctx), diff --git a/idp/handlers/logout.go b/idp/handlers/logout.go index 445a569..81597f0 100644 --- a/idp/handlers/logout.go +++ b/idp/handlers/logout.go @@ -39,7 +39,7 @@ func (h *logoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusFound) } -func NewLogoutHandler(logger *log.Logger, ctx context.Context) *logoutHandler { +func NewLogoutHandler(ctx context.Context, logger *log.Logger) *logoutHandler { return &logoutHandler{ logger: logger, adminClient: ctx.Value(CtxAdminClient).(*admin.Client), diff --git a/idp/services/database.go b/idp/services/database.go new file mode 100644 index 0000000..a7524d0 --- /dev/null +++ b/idp/services/database.go @@ -0,0 +1,46 @@ +package services + +import ( + "context" + "time" + + _ "github.com/go-sql-driver/mysql" + "github.com/jmoiron/sqlx" +) + +type dbContextKey int + +const ( + ctxDbConnection dbContextKey = iota +) + +type DatabaseParams struct { + ConnMaxLifeTime time.Duration + DSN string + MaxOpenConnections int + MaxIdleConnections int +} + +func NewDatabaseParams(dsn string) *DatabaseParams { + return &DatabaseParams{ + DSN: dsn, + ConnMaxLifeTime: time.Minute * 3, + MaxOpenConnections: 10, + MaxIdleConnections: 10, + } +} + +func InitDatabase(ctx context.Context, params *DatabaseParams) (context.Context, error) { + db, err := sqlx.Connect("mysql", params.DSN) + if err != nil { + return nil, err + } + db.SetConnMaxLifetime(params.ConnMaxLifeTime) + db.SetMaxOpenConns(params.MaxOpenConnections) + db.SetMaxIdleConns(params.MaxIdleConnections) + return context.WithValue(ctx, ctxDbConnection, db), nil +} + +func GetDb(ctx context.Context) *sqlx.DB { + return ctx.Value(ctxDbConnection).(*sqlx.DB) +} diff --git a/idp/services/i18n.go b/idp/services/i18n.go index bac9f3c..6484a9c 100644 --- a/idp/services/i18n.go +++ b/idp/services/i18n.go @@ -66,5 +66,9 @@ func AddMessages(ctx context.Context) { ID: "Scope-email-Description", Other: "Access your primary email address.", } + messages["WrongOrLockedUserOrInvalidPassword"] = &i18n.Message{ + ID: "WrongOrLockedUserOrInvalidPassword", + Other: "You entered an invalid username or password or your account has been locked.", + } services.GetMessageCatalog(ctx).AddMessages(messages) } diff --git a/templates/idp/login.gohtml b/templates/idp/login.gohtml index 7f797a3..0c9ee6f 100644 --- a/templates/idp/login.gohtml +++ b/templates/idp/login.gohtml @@ -1,6 +1,7 @@ {{ define "content" }}
{{ .csrfField }} + {{ if .errors.Form}}

{{ .errors.Form }}

{{ end }} {{ if .errors.Email }}

{{ .errors.Email }}

{{ end }}
diff --git a/translate.de.toml b/translate.de.toml index 1ac1197..45bc460 100644 --- a/translate.de.toml +++ b/translate.de.toml @@ -1,16 +1,3 @@ -[IndexGreeting] -hash = "sha1-d4a13058e497fa24143ea96d50d82b818455ef61" -other = "Hallo {{ .User }}" - -[IndexIntroductionText] -hash = "sha1-c2c530e263fc9c38482338ed290aafb496794179" -other = "Das ist eine zugriffsgeschützte Resource" - -[IndexTitle] -hash = "sha1-eccb2b889c068d3f25496c1dad3fb0f88d021bd9" -other = "Willkommen in der Demo-Anwendung" - -[LogoutLabel] -description = "A label on a logout button or link" -hash = "sha1-8acfdeb9a8286f00c8e5dd48471cfdc994807579" -other = "Ausloggen" +[WrongOrLockedUserOrInvalidPassword] +hash = "sha1-87e0a0ac67c6c3a06bed184e10b22aae4d075b64" +other = "Du hast einen ungültigen Nutzernamen oder ein ungültiges Passwort eingegeben oder dein Benutzerkonto wurde gesperrt."