Fix subject handling for login requests

This change implements handling for the case that a login request
retrieved from Hydra has a pre-defined subject. The login request is
rejected if the requested subject is not part of the presented client
certificate.
This commit is contained in:
Jan Dittberner 2024-05-19 08:12:39 +02:00
parent bdf37493d0
commit 7ef12da4fa
5 changed files with 78 additions and 10 deletions

View file

@ -66,26 +66,26 @@ internationalization (i18n) support.
The translation workflow needs the `go18n` binary which can be installed via The translation workflow needs the `go18n` binary which can be installed via
``` ```shell
go install github.com/nicksnyder/go-i18n/v2/goi18n go install github.com/nicksnyder/go-i18n/v2/goi18n
``` ```
To extract new messages from the code run To extract new messages from the code run
``` ```shell
goi18n extract . goi18n extract -outdir translations .
``` ```
Then use Then use
``` ```shell
goi18n merge active.*.toml goi18n merge -outdir translations translations/active.*.toml
``` ```
to create TOML files for translation as `translate.<locale>.toml`. After to create TOML files for translation as `translate.<locale>.toml`. After
translating the messages run translating the messages run
``` ```shell
goi18n merge active.*.toml translate.*.toml goi18n merge active.*.toml translate.*.toml
``` ```

View file

@ -110,6 +110,8 @@ func (h *LoginHandler) handleGet(
return return
} }
usableEmails := certEmails
defer func() { _ = response.Body.Close() }() defer func() { _ = response.Body.Close() }()
h.logger.Debug( h.logger.Debug(
@ -117,7 +119,28 @@ func (h *LoginHandler) handleGet(
"response", response.Status, "login_request", oAuth2LoginRequest, "response", response.Status, "login_request", oAuth2LoginRequest,
) )
h.renderRequestForClientCert(w, r, certEmails, localizer, oAuth2LoginRequest) if subject, ok := oAuth2LoginRequest.GetSubjectOk(); ok && *subject != "" {
h.logger.Info("oauth2LoginRequest expects subject", "subject", *subject)
subjectInCert := false
for _, email := range certEmails {
if *subject == email {
subjectInCert = true
break
}
}
if !subjectInCert {
h.rejectLoginMissingSubject(w, r, challenge, localizer, *subject)
return
}
usableEmails = []string{*subject}
}
h.renderRequestForClientCert(w, r, usableEmails, localizer, oAuth2LoginRequest)
} }
type FlashMessage struct { type FlashMessage struct {
@ -234,7 +257,7 @@ func (h *LoginHandler) rejectLogin(
r.Context(), r.Context(),
).LoginChallenge(challenge).RejectOAuth2Request(*rejectRequest).Execute() ).LoginChallenge(challenge).RejectOAuth2Request(*rejectRequest).Execute()
if err != nil { if err != nil {
h.logger.Error("error getting reject login request", "error", err) h.logger.Error("error sending reject login request", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
@ -242,8 +265,35 @@ func (h *LoginHandler) rejectLogin(
defer func() { _ = response.Body.Close() }() defer func() { _ = response.Body.Close() }()
h.logger.Debug( h.logger.DebugContext(
"go response for RejectOAuth2LoginRequest", r.Context(),
"got response for RejectOAuth2LoginRequest",
"response", response.Status, "reject_login_request", rejectLoginRequest,
)
w.Header().Set("Location", rejectLoginRequest.GetRedirectTo())
w.WriteHeader(http.StatusFound)
}
func (h *LoginHandler) rejectLoginMissingSubject(w http.ResponseWriter, r *http.Request, challenge string, localizer *i18n.Localizer, subject string) {
rejectRequest := client.NewRejectOAuth2RequestWithDefaults()
rejectRequest.SetErrorDescription(h.trans.LookupMessage("LoginDeniedSubjectMissing", map[string]interface{}{"Subject": subject}, localizer))
rejectRequest.SetErrorHint(h.trans.LookupMessage("HintChooseDifferentClientCertificate", nil, localizer))
rejectRequest.SetStatusCode(http.StatusForbidden)
rejectLoginRequest, response, err := h.adminClient.RejectOAuth2LoginRequest(r.Context()).LoginChallenge(challenge).RejectOAuth2Request(*rejectRequest).Execute()
if err != nil {
h.logger.Error("error sending reject login request", "error", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
defer func() { _ = response.Body.Close() }()
h.logger.DebugContext(
r.Context(),
"got response for RejectOAuth2LoginRequest",
"response", response.Status, "reject_login_request", rejectLoginRequest, "response", response.Status, "reject_login_request", rejectLoginRequest,
) )

View file

@ -312,6 +312,10 @@ func (s *I18NService) AddMessages() error {
ID: "LoginDeniedByUser", ID: "LoginDeniedByUser",
Other: "Login has been denied by the user.", Other: "Login has been denied by the user.",
} }
messages["LoginDeniedSubjectMissing"] = &i18n.Message{
ID: "LoginDeniedSubjectMissing",
Other: "Login has been denied because the requested subject {{ .Subject }} could not be authenticated.",
}
messages["LogoutSuccessfulTitle"] = &i18n.Message{ messages["LogoutSuccessfulTitle"] = &i18n.Message{
ID: "LogoutSuccessfulTitle", ID: "LogoutSuccessfulTitle",
Other: "Logout successful", Other: "Logout successful",
@ -332,6 +336,10 @@ func (s *I18NService) AddMessages() error {
ID: "HintChooseAnIdentityForAuthentication", ID: "HintChooseAnIdentityForAuthentication",
Other: "Choose an identity for authentication.", Other: "Choose an identity for authentication.",
} }
messages["HintChooseDifferentClientCertificate"] = &i18n.Message{
ID: "HintChooseDifferentClientCertificate",
Other: "Choose a different client certificate for authentication.",
}
messages["NoConsentGiven"] = &i18n.Message{ messages["NoConsentGiven"] = &i18n.Message{
ID: "NoConsentGiven", ID: "NoConsentGiven",
Other: "You have not given consent to use your data to any application yet.", Other: "You have not given consent to use your data to any application yet.",

View file

@ -93,6 +93,10 @@ other = "Unbekannter Fehler"
hash = "sha1-7ee5b067009bbedc061274ee50a3027b50a06163" hash = "sha1-7ee5b067009bbedc061274ee50a3027b50a06163"
other = "Wähle eine Identität für die Anmeldung." other = "Wähle eine Identität für die Anmeldung."
[HintChooseDifferentClientCertificate]
hash = "sha1-22002121759ae58afccfccb88383dbe54f94f0a9"
other = "Wähle ein anderes Client-Zertifikat für die Anmeldung."
[IndexTitle] [IndexTitle]
hash = "sha1-c763022b69a8ad58ab42d8ea708192abd85fd8f6" hash = "sha1-c763022b69a8ad58ab42d8ea708192abd85fd8f6"
other = "Willkommen bei deinem Identitätsprovider" other = "Willkommen bei deinem Identitätsprovider"
@ -139,6 +143,10 @@ other = "Unbekannt"
hash = "sha1-bbad650536bfb091ad55d576262bbe4358277c73" hash = "sha1-bbad650536bfb091ad55d576262bbe4358277c73"
other = "Die Anmeldung wurde durch den Nutzer abgelehnt." other = "Die Anmeldung wurde durch den Nutzer abgelehnt."
[LoginDeniedSubjectMissing]
hash = "sha1-c787e750515612fd695d6a6beeb3a07ec381f3e7"
other = "Die Anmeldung wurde abgelehnt, weil die angeforderte Identität {{ .Subject }} nicht bestätigt werden konnte."
[LoginTitle] [LoginTitle]
hash = "sha1-9a24c8b64e047edc13f3c41ef7785bb2044a6d69" hash = "sha1-9a24c8b64e047edc13f3c41ef7785bb2044a6d69"
other = "Anmelden mit einem Client-Zertifikat" other = "Anmelden mit einem Client-Zertifikat"

View file

@ -8,6 +8,7 @@ ConfirmRevokeTitle = "Revoke consent"
ErrorTitle = "An error has occurred" ErrorTitle = "An error has occurred"
ErrorUnknown = "Unknown error" ErrorUnknown = "Unknown error"
HintChooseAnIdentityForAuthentication = "Choose an identity for authentication." HintChooseAnIdentityForAuthentication = "Choose an identity for authentication."
HintChooseDifferentClientCertificate = "Choose a different client certificate for authentication."
IndexTitle = "Welcome to your identity provider" IndexTitle = "Welcome to your identity provider"
IndexWelcomeMessage = "Go to [manage consent]({{ .ManageConsentHRef }}) to show or revoke consent you have given to client applications." IndexWelcomeMessage = "Go to [manage consent]({{ .ManageConsentHRef }}) to show or revoke consent you have given to client applications."
IntroConsentMoreInformation = "You can find more information about **{{ .client }}** at [its description page]({{ .clientLink }})." IntroConsentMoreInformation = "You can find more information about **{{ .client }}** at [its description page]({{ .clientLink }})."
@ -17,6 +18,7 @@ LabelNever = "Never"
LabelSubmit = "Submit" LabelSubmit = "Submit"
LabelUnknown = "Unknown" LabelUnknown = "Unknown"
LoginDeniedByUser = "Login has been denied by the user." LoginDeniedByUser = "Login has been denied by the user."
LoginDeniedSubjectMissing = "Login has been denied because the requested subject {{ .Subject }} could not be authenticated."
LoginTitle = "Authenticate with a client certificate" LoginTitle = "Authenticate with a client certificate"
LogoutSuccessfulText = "You have been logged out successfully." LogoutSuccessfulText = "You have been logged out successfully."
LogoutSuccessfulTitle = "Logout successful" LogoutSuccessfulTitle = "Logout successful"