Basic email sending
This commit is contained in:
@ -57,6 +57,8 @@ type SMTPConfig struct {
|
||||
User string `yaml:"user"`
|
||||
Password string `yaml:"password"`
|
||||
InsecureSkipVerify bool `yaml:"insecureSkipVerify"`
|
||||
SenderAddress string `yaml:"senderAddress"`
|
||||
SenderName string `yaml:"senderName"`
|
||||
}
|
||||
|
||||
type HydraConfig struct {
|
||||
@ -83,9 +85,16 @@ func NewDefault() *Config {
|
||||
IssuerURL: "http://localhost:4444/",
|
||||
RedirectURL: "http://localhost:3000/test/oauth2/callback",
|
||||
},
|
||||
SMTP: SMTPConfig{},
|
||||
SMTP: SMTPConfig{
|
||||
Host: "localhost",
|
||||
Port: 2525,
|
||||
User: "hydra-passwordless",
|
||||
Password: "hydra-passwordless",
|
||||
SenderAddress: "noreply@localhost",
|
||||
SenderName: "noreply",
|
||||
},
|
||||
Hydra: HydraConfig{
|
||||
BaseURL: "http://localhost:4444/",
|
||||
BaseURL: "http://localhost:4445/",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,72 @@
|
||||
package hydra
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
baseURL *url.URL
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
func (c *Client) LoginRequest(challenge string) (*LoginResponse, error) {
|
||||
return nil, nil
|
||||
u := fromURL(*c.baseURL, "/oauth2/auth/requests/login", url.Values{
|
||||
"login_challenge": []string{challenge},
|
||||
})
|
||||
|
||||
res, err := c.http.Get(u)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve login response")
|
||||
}
|
||||
|
||||
if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
|
||||
return nil, errors.Wrapf(ErrUnexpectedHydraResponse, "hydra responded with status code '%d'", res.StatusCode)
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
decoder := json.NewDecoder(res.Body)
|
||||
loginRes := &LoginResponse{}
|
||||
|
||||
if err := decoder.Decode(loginRes); err != nil {
|
||||
return nil, errors.Wrap(err, "could not decode json response")
|
||||
}
|
||||
|
||||
return loginRes, nil
|
||||
}
|
||||
|
||||
func (c *Client) Accept(challenge string) (*AcceptResponse, error) {
|
||||
return nil, nil
|
||||
func (c *Client) AcceptRequest(challenge string, req *AcceptRequest) (*AcceptResponse, error) {
|
||||
u := fromURL(*c.baseURL, "/oauth2/auth/requests/accept", url.Values{
|
||||
"login_challenge": []string{challenge},
|
||||
})
|
||||
|
||||
res := &AcceptResponse{}
|
||||
|
||||
if err := c.putJSON(u, req, res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *Client) RejectRequest(challenge string) (*RejectResponse, error) {
|
||||
return nil, nil
|
||||
func (c *Client) RejectRequest(challenge string, req *RejectRequest) (*RejectResponse, error) {
|
||||
u := fromURL(*c.baseURL, "/oauth2/auth/requests/reject", url.Values{
|
||||
"login_challenge": []string{challenge},
|
||||
})
|
||||
|
||||
res := &RejectResponse{}
|
||||
|
||||
if err := c.putJSON(u, req, res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *Client) LogoutRequest(challenge string) (*LogoutResponse, error) {
|
||||
@ -51,7 +98,47 @@ func (c *Client) challenge(r *http.Request, name string) (string, error) {
|
||||
return challenge, nil
|
||||
}
|
||||
|
||||
func NewClient(baseURL string, httpTimeout time.Duration) *Client {
|
||||
func (c *Client) putJSON(u string, payload interface{}, result interface{}) error {
|
||||
var buf bytes.Buffer
|
||||
|
||||
encoder := json.NewEncoder(&buf)
|
||||
if err := encoder.Encode(payload); err != nil {
|
||||
return errors.Wrap(err, "could not encode request body")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("PUT", u, &buf)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create request")
|
||||
}
|
||||
|
||||
res, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve login response")
|
||||
}
|
||||
|
||||
if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
|
||||
return errors.Wrapf(ErrUnexpectedHydraResponse, "hydra responded with status code '%d'", res.StatusCode)
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
decoder := json.NewDecoder(res.Body)
|
||||
|
||||
if err := decoder.Decode(result); err != nil {
|
||||
return errors.Wrap(err, "could not decode json response")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fromURL(url url.URL, path string, query url.Values) string {
|
||||
url.Path = path
|
||||
url.RawQuery = query.Encode()
|
||||
|
||||
return url.String()
|
||||
}
|
||||
|
||||
func NewClient(baseURL *url.URL, httpTimeout time.Duration) *Client {
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
http: &http.Client{
|
||||
|
@ -3,5 +3,6 @@ package hydra
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrChallengeNotFound = errors.New("challenge not found")
|
||||
ErrUnexpectedHydraResponse = errors.New("unexpected hydra response")
|
||||
ErrChallengeNotFound = errors.New("challenge not found")
|
||||
)
|
||||
|
@ -1,15 +1,30 @@
|
||||
package hydra
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
)
|
||||
|
||||
func ServiceProvider(baseURL string, httpTimeout time.Duration) service.Provider {
|
||||
func ServiceProvider(rawBaseURL string, httpTimeout time.Duration) service.Provider {
|
||||
var (
|
||||
baseURL *url.URL
|
||||
err error
|
||||
)
|
||||
|
||||
baseURL, err = url.Parse(rawBaseURL)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "could not parse base url")
|
||||
}
|
||||
|
||||
client := NewClient(baseURL, httpTimeout)
|
||||
|
||||
return func(ctn *service.Container) (interface{}, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
}
|
||||
|
13
internal/hydra/request.go
Normal file
13
internal/hydra/request.go
Normal file
@ -0,0 +1,13 @@
|
||||
package hydra
|
||||
|
||||
type AcceptRequest struct {
|
||||
Subject string `json:"subject"`
|
||||
Remember bool `json:"remember"`
|
||||
RememberFor int `json:"remember_for"`
|
||||
ACR string `json:"acr"`
|
||||
}
|
||||
|
||||
type RejectRequest struct {
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
@ -1,12 +1,69 @@
|
||||
package hydra
|
||||
|
||||
import "time"
|
||||
|
||||
// https://www.ory.sh/hydra/docs/reference/api#get-a-login-request
|
||||
|
||||
type ClientResponseFragment struct {
|
||||
AllowCORSOrigins []string `json:"allowed_cors_origins"`
|
||||
Audience []string `json:"audience"`
|
||||
BackChannelLogoutSessionRequired bool `json:"backchannel_logout_session_required"`
|
||||
BackChannelLogoutURI string `json:"backchannel_logout_uri"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientName string `json:"client_name"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
ClientSecretExpiresAt int `json:"client_secret_expires_at"`
|
||||
ClientURI string `json:"client_uri"`
|
||||
Contacts []string `json:"contacts"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
FrontChannelLogoutSessionRequired bool `json:"frontchannel_logout_session_required"`
|
||||
FrontChannelLogoutURL string `json:"frontchannel_logout_uri"`
|
||||
GrantTypes []string `json:"grant_types"`
|
||||
JWKS map[string]interface{} `json:"jwks"`
|
||||
JwksURI string `json:"jwks_uri"`
|
||||
LogoURI string `json:"logo_uri"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
Owner string `json:"owner"`
|
||||
PolicyURI string `json:"policy_uri"`
|
||||
PostLogoutRedirectURIs []string `json:"post_logout_redirect_uris"`
|
||||
RedirectURIs []string `json:"redirect_uris"`
|
||||
RequestObjectSigningAlg string `json:"request_object_signing_alg"`
|
||||
RequestURIs []string `json:"request_uris"`
|
||||
ResponseTypes []string `json:"response_types"`
|
||||
Scope string `json:"scope"`
|
||||
SectorIdentifierURI string `json:"sector_identifier_uri"`
|
||||
SubjectType string `json:"subject_type"`
|
||||
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
|
||||
TosURI string `json:"tos_uri"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
UserInfoSignedResponseAlg string `json:"userinfo_signed_response_alg"`
|
||||
}
|
||||
|
||||
type OidcContextResponseFragment struct {
|
||||
ACRValues []string `json:"acr_values"`
|
||||
Display string `json:"display"`
|
||||
IDTokenHintClaims map[string]interface{} `json:"id_token_hint_claims"`
|
||||
LoginHint string `json:"login_hint"`
|
||||
UILocales []string `json:"ui_locales"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
Challenge string `json:"challenge"`
|
||||
Skip bool `json:"skip"`
|
||||
Subject string `json:"subject"`
|
||||
Client ClientResponseFragment `json:"client"`
|
||||
RequestURL string `json:"request_url"`
|
||||
RequestedScope []string `json:"requested_scope"`
|
||||
OidcContext OidcContextResponseFragment `json:"oidc_context"`
|
||||
RequestedAccessTokenAudience string `json:"requested_access_token_audience"`
|
||||
SessionID string `json:"session_id"`
|
||||
}
|
||||
|
||||
type AcceptResponse struct {
|
||||
}
|
||||
|
||||
type RejectResponse struct {
|
||||
RedirectTo string `json:"redirect_to"`
|
||||
}
|
||||
|
||||
type LogoutResponse struct {
|
||||
|
@ -1,11 +1,17 @@
|
||||
package mail
|
||||
|
||||
const (
|
||||
ContentTypeHTML = "text/html"
|
||||
ContentTypeText = "text/plain"
|
||||
)
|
||||
|
||||
type Option struct {
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Password string
|
||||
InsecureSkipVerify bool
|
||||
UseStartTLS bool
|
||||
}
|
||||
|
||||
type OptionFunc func(*Option)
|
||||
@ -14,6 +20,33 @@ type Mailer struct {
|
||||
opt *Option
|
||||
}
|
||||
|
||||
func NewMailer(funcs ...OptionFunc) *Mailer {
|
||||
return &Mailer{}
|
||||
func WithTLS(useStartTLS, insecureSkipVerify bool) OptionFunc {
|
||||
return func(opt *Option) {
|
||||
opt.UseStartTLS = useStartTLS
|
||||
opt.InsecureSkipVerify = insecureSkipVerify
|
||||
}
|
||||
}
|
||||
|
||||
func WithServer(host string, port int) OptionFunc {
|
||||
return func(opt *Option) {
|
||||
opt.Host = host
|
||||
opt.Port = port
|
||||
}
|
||||
}
|
||||
|
||||
func WithCredentials(user, password string) OptionFunc {
|
||||
return func(opt *Option) {
|
||||
opt.User = user
|
||||
opt.Password = password
|
||||
}
|
||||
}
|
||||
|
||||
func NewMailer(funcs ...OptionFunc) *Mailer {
|
||||
opt := &Option{}
|
||||
|
||||
for _, fn := range funcs {
|
||||
fn(opt)
|
||||
}
|
||||
|
||||
return &Mailer{opt}
|
||||
}
|
||||
|
@ -40,10 +40,14 @@ func WithCharset(charset string) func(*SendOption) {
|
||||
}
|
||||
}
|
||||
|
||||
func WithFrom(address string, name string) func(*SendOption) {
|
||||
func WithSender(address string, name string) func(*SendOption) {
|
||||
return WithAddressHeader("From", address, name)
|
||||
}
|
||||
|
||||
func WithSubject(subject string) func(*SendOption) {
|
||||
return WithHeader("Subject", subject)
|
||||
}
|
||||
|
||||
func WithAddressHeader(field, address, name string) func(*SendOption) {
|
||||
return func(opt *SendOption) {
|
||||
opt.AddressHeaders = append(opt.AddressHeaders, AddressHeader{field, address, name})
|
||||
@ -56,14 +60,32 @@ func WithHeader(field string, values ...string) func(*SendOption) {
|
||||
}
|
||||
}
|
||||
|
||||
func WithRecipients(addresses ...string) func(*SendOption) {
|
||||
return WithHeader("To", addresses...)
|
||||
}
|
||||
|
||||
func WithCopies(addresses ...string) func(*SendOption) {
|
||||
return WithHeader("Cc", addresses...)
|
||||
}
|
||||
|
||||
func WithInvisibleCopies(addresses ...string) func(*SendOption) {
|
||||
return WithHeader("Cci", addresses...)
|
||||
}
|
||||
|
||||
func WithBody(contentType string, content string, setting gomail.PartSetting) func(*SendOption) {
|
||||
return func(opt *SendOption) {
|
||||
if setting == nil {
|
||||
setting = gomail.SetPartEncoding(gomail.Unencoded)
|
||||
}
|
||||
opt.Body = Body{contentType, content, setting}
|
||||
}
|
||||
}
|
||||
|
||||
func WithAlternativeBody(contentType string, content string, setting gomail.PartSetting) func(*SendOption) {
|
||||
return func(opt *SendOption) {
|
||||
if setting == nil {
|
||||
setting = gomail.SetPartEncoding(gomail.Unencoded)
|
||||
}
|
||||
opt.AlternativeBodies = append(opt.AlternativeBodies, Body{contentType, content, setting})
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,20 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
netMail "net/mail"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
|
||||
"forge.cadoles.com/wpetit/hydra-passwordless/internal/config"
|
||||
"forge.cadoles.com/wpetit/hydra-passwordless/internal/hydra"
|
||||
"forge.cadoles.com/wpetit/hydra-passwordless/internal/mail"
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
"gitlab.com/wpetit/goweb/service/session"
|
||||
"gitlab.com/wpetit/goweb/service/template"
|
||||
)
|
||||
|
||||
@ -32,12 +38,25 @@ func serveLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||
panic(errors.Wrap(err, "could not retrieve hydra login response"))
|
||||
}
|
||||
|
||||
spew.Dump(res)
|
||||
if res.Skip {
|
||||
res, err := hydr.RejectRequest(challenge, &hydra.RejectRequest{
|
||||
Error: "email_not_validated",
|
||||
ErrorDescription: "The email adress could not be verified.",
|
||||
})
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "could not reject hydra authentication request"))
|
||||
}
|
||||
|
||||
http.Redirect(w, r, res.RedirectTo, http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl := template.Must(ctn)
|
||||
|
||||
data := extendTemplateData(w, r, template.Data{
|
||||
csrf.TemplateTag: csrf.TemplateField(r),
|
||||
"LoginChallenge": challenge,
|
||||
"Email": "",
|
||||
})
|
||||
|
||||
if err := tmpl.RenderPage(w, "login.html.tmpl", data); err != nil {
|
||||
@ -48,6 +67,77 @@ func serveLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||
func handleLoginForm(w http.ResponseWriter, r *http.Request) {
|
||||
ctn := container.Must(r.Context())
|
||||
tmpl := template.Must(ctn)
|
||||
hydr := hydra.Must(ctn)
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
email := r.Form.Get("email")
|
||||
challenge := r.Form.Get("challenge")
|
||||
|
||||
renderFlashError := func(message string) {
|
||||
sess, err := session.Must(ctn).Get(w, r)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "could not retrieve session"))
|
||||
}
|
||||
|
||||
sess.AddFlash(session.FlashError, message)
|
||||
|
||||
if err := sess.Save(w, r); err != nil {
|
||||
panic(errors.Wrap(err, "could not save session"))
|
||||
}
|
||||
|
||||
data := extendTemplateData(w, r, template.Data{
|
||||
csrf.TemplateTag: csrf.TemplateField(r),
|
||||
"LoginChallenge": challenge,
|
||||
"Email": email,
|
||||
})
|
||||
|
||||
if err := tmpl.RenderPage(w, "login.html.tmpl", data); err != nil {
|
||||
panic(errors.Wrapf(err, "could not render '%s' page", r.URL.Path))
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := netMail.ParseAddress(email); err != nil {
|
||||
renderFlashError("Veuillez saisir une adresse courriel valide")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if challenge == "" {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
res, err := hydr.LoginRequest(challenge)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "could not retrieve hydra login response"))
|
||||
}
|
||||
|
||||
spew.Dump(res)
|
||||
|
||||
ml := mail.Must(ctn)
|
||||
conf := config.Must(ctn)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Render(&buf, "verification_email.html.tmpl", template.Data{}); err != nil {
|
||||
panic(errors.Wrap(err, "could not render email template"))
|
||||
}
|
||||
|
||||
err = ml.Send(
|
||||
mail.WithSender(conf.SMTP.SenderAddress, conf.SMTP.SenderName),
|
||||
mail.WithRecipients(email),
|
||||
mail.WithSubject(fmt.Sprintf("[Authentification]")),
|
||||
mail.WithBody(mail.ContentTypeHTML, buf.String(), nil),
|
||||
mail.WithAlternativeBody(mail.ContentTypeText, "", nil),
|
||||
)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "could not send email"))
|
||||
}
|
||||
|
||||
data := extendTemplateData(w, r, template.Data{})
|
||||
|
||||
|
Reference in New Issue
Block a user