270 lines
8.5 KiB
Go
270 lines
8.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"strconv"
|
|
"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"
|
|
"github.com/ory/hydra-client-go/client/admin"
|
|
"github.com/ory/hydra-client-go/models"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"git.cacert.org/oidc_login/common/handlers"
|
|
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
|
|
}
|
|
|
|
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)
|
|
accept := r.Header.Get("Accept-Language")
|
|
localizer := i18n.NewLocalizer(h.bundle, accept)
|
|
|
|
// retrieve consent information
|
|
consentData, err := h.adminClient.GetConsentRequest(
|
|
admin.NewGetConsentRequestParams().WithConsentChallenge(challenge))
|
|
if err != nil {
|
|
h.logger.Errorf("error getting consent information: %v", err)
|
|
var errorDetails *handlers.ErrorDetails
|
|
switch v := err.(type) {
|
|
case *admin.GetConsentRequestConflict:
|
|
errorDetails = &handlers.ErrorDetails{
|
|
ErrorMessage: *v.Payload.Error,
|
|
ErrorDetails: []string{v.Payload.ErrorDescription},
|
|
}
|
|
if v.Payload.StatusCode != 0 {
|
|
errorDetails.ErrorCode = strconv.Itoa(int(v.Payload.StatusCode))
|
|
}
|
|
break
|
|
default:
|
|
errorDetails = &handlers.ErrorDetails{
|
|
ErrorMessage: "could not get consent details",
|
|
ErrorDetails: []string{http.StatusText(http.StatusInternalServerError)},
|
|
}
|
|
}
|
|
handlers.GetErrorBucket(r).AddError(errorDetails)
|
|
return
|
|
}
|
|
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
h.renderConsentForm(w, r, consentData, err, localizer)
|
|
break
|
|
case http.MethodPost:
|
|
var consentInfo ConsentInformation
|
|
|
|
// validate input
|
|
decoder := form.NewDecoder()
|
|
if err := decoder.Decode(&consentInfo, r.Form); err != nil {
|
|
h.logger.Error(err)
|
|
http.Error(
|
|
w,
|
|
http.StatusText(http.StatusInternalServerError),
|
|
http.StatusInternalServerError,
|
|
)
|
|
return
|
|
}
|
|
|
|
if consentInfo.ConsentChecked {
|
|
idTokenData := make(map[string]interface{}, 0)
|
|
|
|
db := services.GetDb(h.context)
|
|
|
|
stmt, err := db.PreparexContext(
|
|
r.Context(),
|
|
`SELECT email, verified, fname, mname, lname, dob, language, modified
|
|
FROM users
|
|
WHERE uniqueid = ?
|
|
AND locked = 0`,
|
|
)
|
|
if err != nil {
|
|
h.logger.Errorf("error preparing user information SQL: %v", err)
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
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(
|
|
&models.RejectRequest{}))
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (h *consentHandler) mapRequestedScope(scope models.StringSlicePipeDelimiter, localizer *i18n.Localizer) []*scopeWithLabel {
|
|
result := make([]*scopeWithLabel, 0)
|
|
for _, scopeName := range scope {
|
|
result = append(result, &scopeWithLabel{Name: scopeName, Label: h.messageCatalog.LookupMessage(
|
|
fmt.Sprintf("Scope-%s-Description", scopeName), nil, localizer)})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func NewConsentHandler(ctx context.Context, logger *log.Logger) (*consentHandler, error) {
|
|
consentTemplate, err := template.ParseFiles(
|
|
"templates/idp/base.gohtml", "templates/idp/consent.gohtml")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &consentHandler{
|
|
adminClient: ctx.Value(CtxAdminClient).(*admin.Client),
|
|
bundle: commonServices.GetI18nBundle(ctx),
|
|
consentTemplate: consentTemplate,
|
|
context: ctx,
|
|
logger: logger,
|
|
messageCatalog: commonServices.GetMessageCatalog(ctx),
|
|
}, nil
|
|
}
|