Files
remark42/backend/vendor/github.com/go-pkgz/auth/provider/verify.go
T

229 lines
6.5 KiB
Go

package provider
import (
"bytes"
"crypto/sha1"
"fmt"
"html/template"
"net/http"
"strings"
"time"
"github.com/go-pkgz/rest"
"github.com/golang-jwt/jwt"
"github.com/microcosm-cc/bluemonday"
"github.com/go-pkgz/auth/avatar"
"github.com/go-pkgz/auth/logger"
"github.com/go-pkgz/auth/token"
)
// VerifyHandler implements non-oauth2 provider authorizing users with some confirmation.
// can be email, IM or anything else implementing Sender interface
type VerifyHandler struct {
logger.L
ProviderName string
TokenService VerifTokenService
Issuer string
AvatarSaver AvatarSaver
Sender Sender
Template string
UseGravatar bool
}
// Sender defines interface to send emails
type Sender interface {
Send(address, text string) error
}
// SenderFunc type is an adapter to allow the use of ordinary functions as Sender.
type SenderFunc func(address, text string) error
// Send calls f(address,text) to implement Sender interface
func (f SenderFunc) Send(address, text string) error {
return f(address, text)
}
// VerifTokenService defines interface accessing tokens
type VerifTokenService interface {
Token(claims token.Claims) (string, error)
Parse(tokenString string) (claims token.Claims, err error)
IsExpired(claims token.Claims) bool
Set(w http.ResponseWriter, claims token.Claims) (token.Claims, error)
Reset(w http.ResponseWriter)
}
// Name of the handler
func (e VerifyHandler) Name() string { return e.ProviderName }
// LoginHandler gets name and address from query, makes confirmation token and sends it to user.
// In case if confirmation token presented in the query uses it to create auth token
func (e VerifyHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
// GET /login?site=site&user=name&address=someone@example.com
tkn := r.URL.Query().Get("token")
if tkn == "" { // no token, ask confirmation via email
e.sendConfirmation(w, r)
return
}
// confirmation token presented
// GET /login?token=confirmation-jwt&sess=1
confClaims, err := e.TokenService.Parse(tkn)
if err != nil {
rest.SendErrorJSON(w, r, e.L, http.StatusForbidden, err, "failed to verify confirmation token")
return
}
if e.TokenService.IsExpired(confClaims) {
rest.SendErrorJSON(w, r, e.L, http.StatusForbidden, fmt.Errorf("expired"), "failed to verify confirmation token")
return
}
elems := strings.Split(confClaims.Handshake.ID, "::")
if len(elems) != 2 {
rest.SendErrorJSON(w, r, e.L, http.StatusBadRequest, fmt.Errorf("%s", confClaims.Handshake.ID), "invalid handshake token")
return
}
user, address := elems[0], elems[1]
sessOnly := r.URL.Query().Get("sess") == "1"
u := token.User{
Name: user,
ID: e.ProviderName + "_" + token.HashID(sha1.New(), address),
}
// try to get gravatar for email
if e.UseGravatar && strings.Contains(address, "@") { // TODO: better email check to avoid silly hits to gravatar api
if picURL, e := avatar.GetGravatarURL(address); e == nil {
u.Picture = picURL
}
}
if u, err = setAvatar(e.AvatarSaver, u, &http.Client{Timeout: 5 * time.Second}); err != nil {
rest.SendErrorJSON(w, r, e.L, http.StatusInternalServerError, err, "failed to save avatar to proxy")
return
}
cid, err := randToken()
if err != nil {
rest.SendErrorJSON(w, r, e.L, http.StatusInternalServerError, err, "can't make token id")
return
}
claims := token.Claims{
User: &u,
StandardClaims: jwt.StandardClaims{
Id: cid,
Issuer: e.Issuer,
Audience: confClaims.Audience,
},
SessionOnly: sessOnly,
}
if _, err = e.TokenService.Set(w, claims); err != nil {
rest.SendErrorJSON(w, r, e.L, http.StatusInternalServerError, err, "failed to set token")
return
}
if confClaims.Handshake != nil && confClaims.Handshake.From != "" {
http.Redirect(w, r, confClaims.Handshake.From, http.StatusTemporaryRedirect)
return
}
rest.RenderJSON(w, claims.User)
}
// GET /login?site=site&user=name&address=someone@example.com
func (e VerifyHandler) sendConfirmation(w http.ResponseWriter, r *http.Request) {
user, address := r.URL.Query().Get("user"), r.URL.Query().Get("address")
user = e.sanitize(user)
address = e.sanitize(address)
if user == "" || address == "" {
rest.SendErrorJSON(w, r, e.L, http.StatusBadRequest, fmt.Errorf("wrong request"), "can't get user and address")
return
}
claims := token.Claims{
Handshake: &token.Handshake{
State: "",
ID: user + "::" + address,
},
SessionOnly: r.URL.Query().Get("session") != "" && r.URL.Query().Get("session") != "0",
StandardClaims: jwt.StandardClaims{
Audience: e.sanitize(r.URL.Query().Get("site")),
ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
Issuer: e.Issuer,
},
}
tkn, err := e.TokenService.Token(claims)
if err != nil {
rest.SendErrorJSON(w, r, e.L, http.StatusForbidden, err, "failed to make login token")
return
}
tmpl := msgTemplate
if e.Template != "" {
tmpl = e.Template
}
emailTmpl, err := template.New("confirm").Parse(tmpl)
if err != nil {
rest.SendErrorJSON(w, r, e.L, http.StatusInternalServerError, err, "can't parse confirmation template")
return
}
tmplData := struct {
User string
Address string
Token string
Site string
}{
User: user,
Address: address,
Token: tkn,
Site: r.URL.Query().Get("site"),
}
buf := bytes.Buffer{}
if err = emailTmpl.Execute(&buf, tmplData); err != nil {
rest.SendErrorJSON(w, r, e.L, http.StatusInternalServerError, err, "can't execute confirmation template")
return
}
if err := e.Sender.Send(address, buf.String()); err != nil {
rest.SendErrorJSON(w, r, e.L, http.StatusInternalServerError, err, "failed to send confirmation")
return
}
rest.RenderJSON(w, rest.JSON{"user": user, "address": address})
}
// AuthHandler doesn't do anything for direct login as it has no callbacks
func (e VerifyHandler) AuthHandler(w http.ResponseWriter, r *http.Request) {}
// LogoutHandler - GET /logout
func (e VerifyHandler) LogoutHandler(w http.ResponseWriter, r *http.Request) {
e.TokenService.Reset(w)
}
var msgTemplate = `
Confirmation for {{.User}} {{.Address}}, site {{.Site}}
Token: {{.Token}}
`
func (e VerifyHandler) sanitize(inp string) string {
p := bluemonday.UGCPolicy()
res := p.Sanitize(inp)
res = template.HTMLEscapeString(res)
res = strings.ReplaceAll(res, "&", "&")
res = strings.ReplaceAll(res, """, "\"")
res = strings.ReplaceAll(res, "'", "'")
res = strings.ReplaceAll(res, "\n", "")
res = strings.TrimSpace(res)
if len(res) > 128 {
return res[:128]
}
return res
}