Basic email sending
This commit is contained in:
parent
d65a7248d1
commit
81778121fb
23
Makefile
23
Makefile
@ -19,23 +19,20 @@ watch:
|
|||||||
lint:
|
lint:
|
||||||
golangci-lint run --enable-all
|
golangci-lint run --enable-all
|
||||||
|
|
||||||
hydra:
|
up:
|
||||||
docker run \
|
docker-compose up --build
|
||||||
--rm -it \
|
|
||||||
--name hydra-passwordless \
|
down:
|
||||||
-e DSN=memory \
|
docker-compose down -v --remove-orphans
|
||||||
-e URLS_LOGIN=http://localhost:3000/login \
|
|
||||||
-e URLS_CONSENT=http://localhost:3000/consent \
|
|
||||||
-p 4444:4444 \
|
|
||||||
-p 4445:4445 \
|
|
||||||
oryd/hydra:v1.4.2-alpine \
|
|
||||||
serve all \
|
|
||||||
--dangerous-force-http
|
|
||||||
|
|
||||||
create-client:
|
create-client:
|
||||||
docker exec -it hydra-passwordless \
|
docker-compose exec hydra \
|
||||||
sh -c 'HYDRA_URL=http://localhost:4445 hydra clients create -c http://localhost:3000/test/oauth2/callback'
|
sh -c 'HYDRA_URL=http://localhost:4445 hydra clients create -c http://localhost:3000/test/oauth2/callback'
|
||||||
|
|
||||||
|
list-clients:
|
||||||
|
docker-compose exec hydra \
|
||||||
|
sh -c 'HYDRA_URL=http://localhost:4445 hydra clients list'
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf release
|
rm -rf release
|
||||||
rm -rf data
|
rm -rf data
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"forge.cadoles.com/wpetit/hydra-passwordless/internal/config"
|
"forge.cadoles.com/wpetit/hydra-passwordless/internal/config"
|
||||||
"forge.cadoles.com/wpetit/hydra-passwordless/internal/hydra"
|
"forge.cadoles.com/wpetit/hydra-passwordless/internal/hydra"
|
||||||
|
"forge.cadoles.com/wpetit/hydra-passwordless/internal/mail"
|
||||||
"forge.cadoles.com/wpetit/hydra-passwordless/oidc"
|
"forge.cadoles.com/wpetit/hydra-passwordless/oidc"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@ -93,5 +94,11 @@ func getServiceContainer(conf *config.Config) (*service.Container, error) {
|
|||||||
|
|
||||||
ctn.Provide(hydra.ServiceName, hydra.ServiceProvider(conf.Hydra.BaseURL, 30*time.Second))
|
ctn.Provide(hydra.ServiceName, hydra.ServiceProvider(conf.Hydra.BaseURL, 30*time.Second))
|
||||||
|
|
||||||
|
ctn.Provide(mail.ServiceName, mail.ServiceProvider(
|
||||||
|
mail.WithServer(conf.SMTP.Host, conf.SMTP.Port),
|
||||||
|
mail.WithCredentials(conf.SMTP.User, conf.SMTP.Password),
|
||||||
|
mail.WithTLS(conf.SMTP.UseStartTLS, conf.SMTP.InsecureSkipVerify),
|
||||||
|
))
|
||||||
|
|
||||||
return ctn, nil
|
return ctn, nil
|
||||||
}
|
}
|
||||||
|
332
cmd/server/template/blocks/email.html.tmpl
Normal file
332
cmd/server/template/blocks/email.html.tmpl
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
{{define "email"}}
|
||||||
|
<!--
|
||||||
|
|
||||||
|
Template based on https://github.com/leemunroe/responsive-html-email-template
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) [2013] [Lee Munroe]
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>{{- block "title" . -}}{{- end -}}</title>
|
||||||
|
<style>
|
||||||
|
/* -------------------------------------
|
||||||
|
GLOBAL RESETS
|
||||||
|
------------------------------------- */
|
||||||
|
img {
|
||||||
|
border: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
max-width: 100%; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
font-family: sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%; }
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
width: 100%; }
|
||||||
|
table td {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
vertical-align: top; }
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
BODY & CONTAINER
|
||||||
|
------------------------------------- */
|
||||||
|
|
||||||
|
.body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
width: 100%; }
|
||||||
|
|
||||||
|
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
/* makes it centered */
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 580px; }
|
||||||
|
|
||||||
|
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||||
|
.content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px; }
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
HEADER, FOOTER, MAIN
|
||||||
|
------------------------------------- */
|
||||||
|
.main {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 100%; }
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20px; }
|
||||||
|
|
||||||
|
.content-block {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
clear: both;
|
||||||
|
margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%; }
|
||||||
|
.footer td,
|
||||||
|
.footer p,
|
||||||
|
.footer span,
|
||||||
|
.footer a {
|
||||||
|
color: #999999;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center; }
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
TYPOGRAPHY
|
||||||
|
------------------------------------- */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
color: #000000;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 30px; }
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 35px;
|
||||||
|
font-weight: 300;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: capitalize; }
|
||||||
|
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 15px; }
|
||||||
|
p li,
|
||||||
|
ul li,
|
||||||
|
ol li {
|
||||||
|
list-style-position: inside;
|
||||||
|
margin-left: 5px; }
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: underline; }
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
BUTTONS
|
||||||
|
------------------------------------- */
|
||||||
|
.btn {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%; }
|
||||||
|
.btn > tbody > tr > td {
|
||||||
|
padding-bottom: 15px; }
|
||||||
|
.btn table {
|
||||||
|
width: auto; }
|
||||||
|
.btn table td {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center; }
|
||||||
|
.btn a {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: solid 1px #3498db;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #3498db;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 25px;
|
||||||
|
text-decoration: none; }
|
||||||
|
|
||||||
|
.btn-primary table td {
|
||||||
|
background-color: #3498db; }
|
||||||
|
|
||||||
|
.btn-primary a {
|
||||||
|
background-color: #3498db;
|
||||||
|
border-color: #3498db;
|
||||||
|
color: #ffffff; }
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
OTHER STYLES THAT MIGHT BE USEFUL
|
||||||
|
------------------------------------- */
|
||||||
|
.last {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
|
||||||
|
.first {
|
||||||
|
margin-top: 0; }
|
||||||
|
|
||||||
|
.align-center {
|
||||||
|
text-align: center; }
|
||||||
|
|
||||||
|
.align-right {
|
||||||
|
text-align: right; }
|
||||||
|
|
||||||
|
.align-left {
|
||||||
|
text-align: left; }
|
||||||
|
|
||||||
|
.clear {
|
||||||
|
clear: both; }
|
||||||
|
|
||||||
|
.mt0 {
|
||||||
|
margin-top: 0; }
|
||||||
|
|
||||||
|
.mb0 {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
|
||||||
|
.preheader {
|
||||||
|
color: transparent;
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0; }
|
||||||
|
|
||||||
|
.powered-by a {
|
||||||
|
text-decoration: none; }
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #f6f6f6;
|
||||||
|
Margin: 20px 0; }
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||||
|
------------------------------------- */
|
||||||
|
@media only screen and (max-width: 620px) {
|
||||||
|
table[class=body] h1 {
|
||||||
|
font-size: 28px !important;
|
||||||
|
margin-bottom: 10px !important; }
|
||||||
|
table[class=body] p,
|
||||||
|
table[class=body] ul,
|
||||||
|
table[class=body] ol,
|
||||||
|
table[class=body] td,
|
||||||
|
table[class=body] span,
|
||||||
|
table[class=body] a {
|
||||||
|
font-size: 16px !important; }
|
||||||
|
table[class=body] .wrapper,
|
||||||
|
table[class=body] .article {
|
||||||
|
padding: 10px !important; }
|
||||||
|
table[class=body] .content {
|
||||||
|
padding: 0 !important; }
|
||||||
|
table[class=body] .container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .main {
|
||||||
|
border-left-width: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-right-width: 0 !important; }
|
||||||
|
table[class=body] .btn table {
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .btn a {
|
||||||
|
width: 100% !important; }
|
||||||
|
table[class=body] .img-responsive {
|
||||||
|
height: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: auto !important; }}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
PRESERVE THESE STYLES IN THE HEAD
|
||||||
|
------------------------------------- */
|
||||||
|
@media all {
|
||||||
|
.ExternalClass {
|
||||||
|
width: 100%; }
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height: 100%; }
|
||||||
|
.apple-link a {
|
||||||
|
color: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
text-decoration: none !important; }
|
||||||
|
.btn-primary table td:hover {
|
||||||
|
background-color: #34495e !important; }
|
||||||
|
.btn-primary a:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
border-color: #34495e !important; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||||
|
<tr>
|
||||||
|
<td> </td>
|
||||||
|
<td class="container">
|
||||||
|
<div class="content">
|
||||||
|
<!-- START CENTERED WHITE CONTAINER -->
|
||||||
|
<span class="preheader">{{- block "title" . -}}{{- end -}}</span>
|
||||||
|
<table class="main">
|
||||||
|
<!-- START MAIN CONTENT AREA -->
|
||||||
|
{{- block "content" . -}}{{- end -}}
|
||||||
|
<!-- END MAIN CONTENT AREA -->
|
||||||
|
</table>
|
||||||
|
<!-- START FOOTER -->
|
||||||
|
<div class="footer">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
{{- block "footer" . -}}{{- end -}}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!-- END FOOTER -->
|
||||||
|
<!-- END CENTERED WHITE CONTAINER -->
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
@ -5,6 +5,7 @@
|
|||||||
<div class="container has-text-centered">
|
<div class="container has-text-centered">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-4 is-offset-4">
|
<div class="column is-4 is-offset-4">
|
||||||
|
{{template "flash" .}}
|
||||||
<p class="has-text-black title">
|
<p class="has-text-black title">
|
||||||
Connexion
|
Connexion
|
||||||
</p>
|
</p>
|
||||||
@ -16,11 +17,12 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input is-large" type="email"
|
<input class="input is-large" type="email"
|
||||||
id="email"
|
id="email" value="{{ .Email }}"
|
||||||
name="email" placeholder="john.doe@email.com" />
|
name="email" placeholder="john.doe@email.com" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ .csrfField }}
|
{{ .csrfField }}
|
||||||
|
<input name="challenge" type="hidden" value="{{ .LoginChallenge }}" />
|
||||||
<button type="submit" class="button is-link is-medium is-block is-fullwidth">Envoyer</button>
|
<button type="submit" class="button is-link is-medium is-block is-fullwidth">Envoyer</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
30
cmd/server/template/layouts/verification_email.html.tmpl
Normal file
30
cmd/server/template/layouts/verification_email.html.tmpl
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{{define "content"}}
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p>Bonjour {{ .Email }}</p>
|
||||||
|
<p>Vous avez demandé à accéder à l'application "{{ .AppTitle }}". Cliquez sur le lien ci dessous pour vous authentifier. </p>
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td> <a href="{{ .VerificationLink }}" target="_blank">Accéder à l'application</a> </td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
{{template "email" .}}
|
20
docker-compose.yml
Normal file
20
docker-compose.yml
Normal file
@ -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
|
3
go.mod
3
go.mod
@ -11,10 +11,9 @@ require (
|
|||||||
github.com/gorilla/sessions v1.2.0
|
github.com/gorilla/sessions v1.2.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
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
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
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/mail.v2 v2.3.1
|
||||||
gopkg.in/square/go-jose.v2 v2.4.1 // indirect
|
gopkg.in/square/go-jose.v2 v2.4.1 // indirect
|
||||||
gopkg.in/yaml.v2 v2.2.8
|
gopkg.in/yaml.v2 v2.2.8
|
||||||
|
13
go.sum
13
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/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/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/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.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 h1:ETj3cggsVIY2Xao5ExCu6YhEh5MD6JTfcBzS37R260w=
|
||||||
github.com/go-chi/chi v4.1.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
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/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/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/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/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
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/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/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=
|
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.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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/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 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
|
||||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
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/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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
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/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/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
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-20200415164411-636b2dbf8ff7 h1:lHdiFEjVYTDd6cLfp1fEJUtRFJFyffYCuQFvauZm+OM=
|
||||||
gitlab.com/wpetit/goweb v0.0.0-20200317131025-42aba649c833/go.mod h1:wqXhN3jywegFzw33pEFAEbsXnshFx0nJ+aXTi4pCtIQ=
|
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.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
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/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.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.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/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-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/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 h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
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 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/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/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/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=
|
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||||
|
@ -57,6 +57,8 @@ type SMTPConfig struct {
|
|||||||
User string `yaml:"user"`
|
User string `yaml:"user"`
|
||||||
Password string `yaml:"password"`
|
Password string `yaml:"password"`
|
||||||
InsecureSkipVerify bool `yaml:"insecureSkipVerify"`
|
InsecureSkipVerify bool `yaml:"insecureSkipVerify"`
|
||||||
|
SenderAddress string `yaml:"senderAddress"`
|
||||||
|
SenderName string `yaml:"senderName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HydraConfig struct {
|
type HydraConfig struct {
|
||||||
@ -83,9 +85,16 @@ func NewDefault() *Config {
|
|||||||
IssuerURL: "http://localhost:4444/",
|
IssuerURL: "http://localhost:4444/",
|
||||||
RedirectURL: "http://localhost:3000/test/oauth2/callback",
|
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{
|
Hydra: HydraConfig{
|
||||||
BaseURL: "http://localhost:4444/",
|
BaseURL: "http://localhost:4445/",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,72 @@
|
|||||||
package hydra
|
package hydra
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
baseURL string
|
baseURL *url.URL
|
||||||
http *http.Client
|
http *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) LoginRequest(challenge string) (*LoginResponse, error) {
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Accept(challenge string) (*AcceptResponse, error) {
|
if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
|
||||||
return nil, nil
|
return nil, errors.Wrapf(ErrUnexpectedHydraResponse, "hydra responded with status code '%d'", res.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) RejectRequest(challenge string) (*RejectResponse, error) {
|
defer res.Body.Close()
|
||||||
return nil, nil
|
|
||||||
|
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) 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, 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) {
|
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
|
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{
|
return &Client{
|
||||||
baseURL: baseURL,
|
baseURL: baseURL,
|
||||||
http: &http.Client{
|
http: &http.Client{
|
||||||
|
@ -3,5 +3,6 @@ package hydra
|
|||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
ErrUnexpectedHydraResponse = errors.New("unexpected hydra response")
|
||||||
ErrChallengeNotFound = errors.New("challenge not found")
|
ErrChallengeNotFound = errors.New("challenge not found")
|
||||||
)
|
)
|
||||||
|
@ -1,15 +1,30 @@
|
|||||||
package hydra
|
package hydra
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/service"
|
"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)
|
client := NewClient(baseURL, httpTimeout)
|
||||||
|
|
||||||
return func(ctn *service.Container) (interface{}, error) {
|
return func(ctn *service.Container) (interface{}, error) {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return client, nil
|
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
|
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 {
|
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 AcceptResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type RejectResponse struct {
|
type RejectResponse struct {
|
||||||
|
RedirectTo string `json:"redirect_to"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogoutResponse struct {
|
type LogoutResponse struct {
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
package mail
|
package mail
|
||||||
|
|
||||||
|
const (
|
||||||
|
ContentTypeHTML = "text/html"
|
||||||
|
ContentTypeText = "text/plain"
|
||||||
|
)
|
||||||
|
|
||||||
type Option struct {
|
type Option struct {
|
||||||
Host string
|
Host string
|
||||||
Port int
|
Port int
|
||||||
User string
|
User string
|
||||||
Password string
|
Password string
|
||||||
InsecureSkipVerify bool
|
InsecureSkipVerify bool
|
||||||
|
UseStartTLS bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type OptionFunc func(*Option)
|
type OptionFunc func(*Option)
|
||||||
@ -14,6 +20,33 @@ type Mailer struct {
|
|||||||
opt *Option
|
opt *Option
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMailer(funcs ...OptionFunc) *Mailer {
|
func WithTLS(useStartTLS, insecureSkipVerify bool) OptionFunc {
|
||||||
return &Mailer{}
|
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)
|
return WithAddressHeader("From", address, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithSubject(subject string) func(*SendOption) {
|
||||||
|
return WithHeader("Subject", subject)
|
||||||
|
}
|
||||||
|
|
||||||
func WithAddressHeader(field, address, name string) func(*SendOption) {
|
func WithAddressHeader(field, address, name string) func(*SendOption) {
|
||||||
return func(opt *SendOption) {
|
return func(opt *SendOption) {
|
||||||
opt.AddressHeaders = append(opt.AddressHeaders, AddressHeader{field, address, name})
|
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) {
|
func WithBody(contentType string, content string, setting gomail.PartSetting) func(*SendOption) {
|
||||||
return func(opt *SendOption) {
|
return func(opt *SendOption) {
|
||||||
|
if setting == nil {
|
||||||
|
setting = gomail.SetPartEncoding(gomail.Unencoded)
|
||||||
|
}
|
||||||
opt.Body = Body{contentType, content, setting}
|
opt.Body = Body{contentType, content, setting}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithAlternativeBody(contentType string, content string, setting gomail.PartSetting) func(*SendOption) {
|
func WithAlternativeBody(contentType string, content string, setting gomail.PartSetting) func(*SendOption) {
|
||||||
return func(opt *SendOption) {
|
return func(opt *SendOption) {
|
||||||
|
if setting == nil {
|
||||||
|
setting = gomail.SetPartEncoding(gomail.Unencoded)
|
||||||
|
}
|
||||||
opt.AlternativeBodies = append(opt.AlternativeBodies, Body{contentType, content, setting})
|
opt.AlternativeBodies = append(opt.AlternativeBodies, Body{contentType, content, setting})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,20 @@
|
|||||||
package route
|
package route
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
netMail "net/mail"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"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/hydra"
|
||||||
|
"forge.cadoles.com/wpetit/hydra-passwordless/internal/mail"
|
||||||
"github.com/gorilla/csrf"
|
"github.com/gorilla/csrf"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/middleware/container"
|
"gitlab.com/wpetit/goweb/middleware/container"
|
||||||
|
"gitlab.com/wpetit/goweb/service/session"
|
||||||
"gitlab.com/wpetit/goweb/service/template"
|
"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"))
|
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)
|
tmpl := template.Must(ctn)
|
||||||
|
|
||||||
data := extendTemplateData(w, r, template.Data{
|
data := extendTemplateData(w, r, template.Data{
|
||||||
csrf.TemplateTag: csrf.TemplateField(r),
|
csrf.TemplateTag: csrf.TemplateField(r),
|
||||||
|
"LoginChallenge": challenge,
|
||||||
|
"Email": "",
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := tmpl.RenderPage(w, "login.html.tmpl", data); err != nil {
|
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) {
|
func handleLoginForm(w http.ResponseWriter, r *http.Request) {
|
||||||
ctn := container.Must(r.Context())
|
ctn := container.Must(r.Context())
|
||||||
tmpl := template.Must(ctn)
|
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{})
|
data := extendTemplateData(w, r, template.Data{})
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user