+ {{template "flash" .}}
Connexion
@@ -16,11 +17,12 @@
{{ .csrfField }}
+
Envoyer
diff --git a/cmd/server/template/layouts/verification_email.html.tmpl b/cmd/server/template/layouts/verification_email.html.tmpl
new file mode 100644
index 0000000..fce611d
--- /dev/null
+++ b/cmd/server/template/layouts/verification_email.html.tmpl
@@ -0,0 +1,30 @@
+{{define "content"}}
+
+
+
+
+
+ Bonjour {{ .Email }}
+ Vous avez demandé à accéder à l'application "{{ .AppTitle }}". Cliquez sur le lien ci dessous pour vous authentifier.
+
+
+
+
+
+
+{{end}}
+{{template "email" .}}
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..5226b9e
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,20 @@
+version: '2.4'
+services:
+ hydra:
+ image: oryd/hydra:v1.4.2-alpine
+ environment:
+ DSN: memory
+ URLS_LOGIN: http://localhost:3000/login
+ URLS_CONSENT: http://localhost:3000/consent
+ ports:
+ - 4444:4444
+ - 4445:4445
+ command: serve all --dangerous-force-http
+ smtp:
+ image: bornholm/fake-smtp
+ ports:
+ - 3001:8080
+ - 2525:2525
+ volumes:
+ - /etc/localtime:/etc/localtime:ro
+ - /etc/timezone:/etc/timezone:ro
diff --git a/go.mod b/go.mod
index 140091b..84c2767 100644
--- a/go.mod
+++ b/go.mod
@@ -11,10 +11,9 @@ require (
github.com/gorilla/sessions v1.2.0
github.com/pkg/errors v0.9.1
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
- gitlab.com/wpetit/goweb v0.0.0-20200317131025-42aba649c833
+ gitlab.com/wpetit/goweb v0.0.0-20200415164411-636b2dbf8ff7
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
- gopkg.in/dgrijalva/jwt-go.v3 v3.2.0 // indirect
gopkg.in/mail.v2 v2.3.1
gopkg.in/square/go-jose.v2 v2.4.1 // indirect
gopkg.in/yaml.v2 v2.2.8
diff --git a/go.sum b/go.sum
index dc869a2..3f9a028 100644
--- a/go.sum
+++ b/go.sum
@@ -40,7 +40,6 @@ github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/go-chi/chi v1.0.0 h1:s/kv1cTXfivYjdKJdyUzNGyAWZ/2t7duW1gKn5ivu+c=
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/chi v4.1.0+incompatible h1:ETj3cggsVIY2Xao5ExCu6YhEh5MD6JTfcBzS37R260w=
github.com/go-chi/chi v4.1.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
@@ -83,8 +82,10 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@@ -100,6 +101,7 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
@@ -109,11 +111,12 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
-gitlab.com/wpetit/goweb v0.0.0-20200317131025-42aba649c833 h1:e2HXOwLZOcurBeqA6XwIdXNLZwGN6oXHBhPdhnBrEq8=
-gitlab.com/wpetit/goweb v0.0.0-20200317131025-42aba649c833/go.mod h1:wqXhN3jywegFzw33pEFAEbsXnshFx0nJ+aXTi4pCtIQ=
+gitlab.com/wpetit/goweb v0.0.0-20200415164411-636b2dbf8ff7 h1:lHdiFEjVYTDd6cLfp1fEJUtRFJFyffYCuQFvauZm+OM=
+gitlab.com/wpetit/goweb v0.0.0-20200415164411-636b2dbf8ff7/go.mod h1:wqXhN3jywegFzw33pEFAEbsXnshFx0nJ+aXTi4pCtIQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -175,6 +178,7 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+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/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -224,9 +228,8 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/dgrijalva/jwt-go.v3 v3.2.0 h1:N46iQqOtHry7Hxzb9PGrP68oovQmj7EhudNoKHvbOvI=
-gopkg.in/dgrijalva/jwt-go.v3 v3.2.0/go.mod h1:hdNXC2Z9yC029rvsQ/on2ZNQ44Z2XToVhpXXbR+J05A=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
diff --git a/internal/config/config.go b/internal/config/config.go
index ad39bc2..ee65bd6 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -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/",
},
}
}
diff --git a/internal/hydra/client.go b/internal/hydra/client.go
index 86847eb..28b58d8 100644
--- a/internal/hydra/client.go
+++ b/internal/hydra/client.go
@@ -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{
diff --git a/internal/hydra/error.go b/internal/hydra/error.go
index bb93540..8e08555 100644
--- a/internal/hydra/error.go
+++ b/internal/hydra/error.go
@@ -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")
)
diff --git a/internal/hydra/provider.go b/internal/hydra/provider.go
index 6d37de0..195038e 100644
--- a/internal/hydra/provider.go
+++ b/internal/hydra/provider.go
@@ -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
}
}
diff --git a/internal/hydra/request.go b/internal/hydra/request.go
new file mode 100644
index 0000000..2a30007
--- /dev/null
+++ b/internal/hydra/request.go
@@ -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"`
+}
diff --git a/internal/hydra/response.go b/internal/hydra/response.go
index 3c9aeab..bfbf5d0 100644
--- a/internal/hydra/response.go
+++ b/internal/hydra/response.go
@@ -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 {
diff --git a/internal/mail/mailer.go b/internal/mail/mailer.go
index 822196b..b1dbf3b 100644
--- a/internal/mail/mailer.go
+++ b/internal/mail/mailer.go
@@ -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}
}
diff --git a/internal/mail/send.go b/internal/mail/send.go
index 752a2e8..05da094 100644
--- a/internal/mail/send.go
+++ b/internal/mail/send.go
@@ -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})
}
}
diff --git a/internal/route/login.go b/internal/route/login.go
index f40cf42..6db368a 100644
--- a/internal/route/login.go
+++ b/internal/route/login.go
@@ -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{})
diff --git a/modd.conf b/modd.conf
index 7a9944f..8b5ca34 100644
--- a/modd.conf
+++ b/modd.conf
@@ -10,4 +10,8 @@ modd.conf {
**/*.go {
prep: make test
+}
+
+docker-compose.yml {
+ daemon: make up
}
\ No newline at end of file