2022-02-05 14:55:57 +00:00
|
|
|
package controllers
|
|
|
|
|
|
|
|
import (
|
2022-02-05 16:24:07 +00:00
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
2022-02-05 14:55:57 +00:00
|
|
|
"net/http"
|
2022-02-05 16:24:07 +00:00
|
|
|
"net/url"
|
|
|
|
"time"
|
2022-02-05 14:55:57 +00:00
|
|
|
|
2022-02-05 16:24:07 +00:00
|
|
|
"git.jamesravey.me/ravenscroftj/indiescrobble/config"
|
2022-02-06 12:06:19 +00:00
|
|
|
"git.jamesravey.me/ravenscroftj/indiescrobble/models"
|
2022-02-05 14:55:57 +00:00
|
|
|
"github.com/gin-gonic/gin"
|
2022-02-05 16:24:07 +00:00
|
|
|
"github.com/go-chi/jwtauth/v5"
|
|
|
|
"github.com/hacdias/indieauth"
|
|
|
|
"github.com/lestrrat-go/jwx/jwt"
|
2022-02-13 15:49:22 +00:00
|
|
|
"gorm.io/gorm"
|
2022-02-05 14:55:57 +00:00
|
|
|
)
|
|
|
|
|
2022-02-05 16:24:07 +00:00
|
|
|
type IndieAuthManager struct {
|
2022-02-13 15:49:22 +00:00
|
|
|
iac *indieauth.Client
|
2022-02-05 16:24:07 +00:00
|
|
|
jwtAuth *jwtauth.JWTAuth
|
2022-02-13 15:49:22 +00:00
|
|
|
db *gorm.DB
|
2022-02-05 16:24:07 +00:00
|
|
|
}
|
|
|
|
|
2022-02-13 15:49:22 +00:00
|
|
|
func NewIndieAuthManager(db *gorm.DB) *IndieAuthManager {
|
|
|
|
|
2022-02-05 16:24:07 +00:00
|
|
|
config := config.GetConfig()
|
|
|
|
|
|
|
|
iam := new(IndieAuthManager)
|
|
|
|
iam.iac = indieauth.NewClient(config.GetString("indieauth.clientName"), config.GetString("indieauth.redirectURL"), nil)
|
2022-02-13 15:49:22 +00:00
|
|
|
iam.db = db
|
2022-02-05 16:24:07 +00:00
|
|
|
iam.jwtAuth = jwtauth.New("HS256", []byte(config.GetString("jwt.signKey")), []byte(config.GetString("jwt.signKey")))
|
|
|
|
|
|
|
|
return iam
|
|
|
|
}
|
|
|
|
|
2022-02-06 12:06:19 +00:00
|
|
|
func (iam *IndieAuthManager) GetCurrentUser(c *gin.Context) *models.BaseUser {
|
2022-02-05 16:24:07 +00:00
|
|
|
|
|
|
|
jwt, err := c.Cookie("jwt")
|
|
|
|
|
|
|
|
if err != nil {
|
2022-02-06 12:06:19 +00:00
|
|
|
return nil
|
2022-02-13 15:49:22 +00:00
|
|
|
} else {
|
2022-02-05 16:24:07 +00:00
|
|
|
tok, err := iam.jwtAuth.Decode(jwt)
|
|
|
|
|
2022-02-13 15:49:22 +00:00
|
|
|
if err != nil {
|
2022-02-05 16:24:07 +00:00
|
|
|
log.Printf("Failed to decode jwt: %v", err)
|
2022-02-06 12:06:19 +00:00
|
|
|
return nil
|
2022-02-05 16:24:07 +00:00
|
|
|
}
|
|
|
|
|
2022-02-06 12:06:19 +00:00
|
|
|
me, present := tok.Get("user")
|
2022-02-05 16:24:07 +00:00
|
|
|
|
2022-02-13 15:49:22 +00:00
|
|
|
if !present {
|
2022-02-06 12:06:19 +00:00
|
|
|
return nil
|
|
|
|
}
|
2022-02-05 19:59:41 +00:00
|
|
|
|
2022-02-06 12:06:19 +00:00
|
|
|
indietok, present := tok.Get("token")
|
2022-02-05 19:59:41 +00:00
|
|
|
|
2022-02-13 15:49:22 +00:00
|
|
|
if !present {
|
2022-02-06 12:06:19 +00:00
|
|
|
return nil
|
2022-02-05 16:24:07 +00:00
|
|
|
}
|
|
|
|
|
2022-02-13 15:49:22 +00:00
|
|
|
// see if the user exists in the database or set up their profile
|
|
|
|
userRecord := models.User{}
|
|
|
|
result := iam.db.First(&userRecord, models.User{Me: me.(string)})
|
|
|
|
|
|
|
|
if result.Error != nil {
|
|
|
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
|
|
log.Printf("Create new user profile for user %v\n", me)
|
|
|
|
|
|
|
|
// create user record for current user
|
|
|
|
userRecord = models.User{Me: me.(string)}
|
|
|
|
userRecord.GenerateRandomKey()
|
|
|
|
result := iam.db.Create(&userRecord)
|
|
|
|
|
|
|
|
if result.Error != nil {
|
|
|
|
log.Printf("Failed to create user record in db: %v", result.Error)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
log.Printf("Failed to get user from db: %v\n", result.Error)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
user := models.BaseUser{Me: me.(string), Token: indietok.(string), UserRecord: &userRecord}
|
2022-02-06 12:06:19 +00:00
|
|
|
|
|
|
|
return &user
|
|
|
|
|
2022-02-05 16:24:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func (iam *IndieAuthManager) getInformation(c *gin.Context) (*indieauth.AuthInfo, string, error) {
|
|
|
|
|
|
|
|
config := config.GetConfig()
|
|
|
|
|
|
|
|
cookie, err := c.Request.Cookie(config.GetString("indieauth.oauthCookieName"))
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
token, err := jwtauth.VerifyToken(iam.jwtAuth, cookie.Value)
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = jwt.Validate(token)
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
if token.Subject() != config.GetString("indieauth.oauthSubject") {
|
|
|
|
return nil, "", errors.New("invalid subject for oauth token")
|
|
|
|
}
|
|
|
|
|
|
|
|
data, ok := token.Get("data")
|
|
|
|
if !ok || data == nil {
|
|
|
|
return nil, "", errors.New("cannot find 'data' property in token")
|
|
|
|
}
|
|
|
|
|
|
|
|
dataStr, ok := data.(string)
|
|
|
|
if !ok || dataStr == "" {
|
|
|
|
return nil, "", errors.New("cannot find 'data' property in token")
|
|
|
|
}
|
|
|
|
|
|
|
|
var i *indieauth.AuthInfo
|
|
|
|
err = json.Unmarshal([]byte(dataStr), &i)
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete cookie
|
|
|
|
http.SetCookie(c.Writer, &http.Cookie{
|
|
|
|
Name: config.GetString("indieauth.oauthCookieName"),
|
|
|
|
MaxAge: -1,
|
|
|
|
Secure: c.Request.URL.Scheme == "https",
|
|
|
|
Path: "/",
|
|
|
|
HttpOnly: true,
|
|
|
|
})
|
|
|
|
|
|
|
|
redirect, ok := token.Get("redirect")
|
|
|
|
if !ok {
|
|
|
|
return i, "", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
redirectStr, ok := redirect.(string)
|
|
|
|
if !ok || redirectStr == "" {
|
|
|
|
return i, "", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return i, redirectStr, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (iam *IndieAuthManager) saveAuthInfo(w http.ResponseWriter, r *http.Request, i *indieauth.AuthInfo) error {
|
|
|
|
data, err := json.Marshal(i)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
config := config.GetConfig()
|
|
|
|
|
|
|
|
expiration := time.Now().Add(time.Minute * 10)
|
|
|
|
|
|
|
|
_, signed, err := iam.jwtAuth.Encode(map[string]interface{}{
|
|
|
|
jwt.SubjectKey: config.GetString("indieauth.oauthSubject"),
|
|
|
|
jwt.IssuedAtKey: time.Now().Unix(),
|
|
|
|
jwt.ExpirationKey: expiration,
|
|
|
|
"data": string(data),
|
|
|
|
"redirect": r.URL.Query().Get("redirect"),
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
cookie := &http.Cookie{
|
|
|
|
Name: config.GetString("indieauth.oauthCookieName"),
|
|
|
|
Value: string(signed),
|
|
|
|
Expires: expiration,
|
|
|
|
Secure: r.URL.Scheme == "https",
|
|
|
|
HttpOnly: true,
|
|
|
|
Path: "/",
|
|
|
|
SameSite: http.SameSiteLaxMode,
|
|
|
|
}
|
|
|
|
|
|
|
|
http.SetCookie(w, cookie)
|
|
|
|
return nil
|
|
|
|
}
|
2022-02-05 14:55:57 +00:00
|
|
|
|
2022-02-05 19:59:41 +00:00
|
|
|
func (iam *IndieAuthManager) Logout(c *gin.Context) {
|
|
|
|
|
|
|
|
// delete the cookie
|
|
|
|
cookie := &http.Cookie{
|
|
|
|
Name: "jwt",
|
|
|
|
MaxAge: -1,
|
|
|
|
Secure: c.Request.URL.Scheme == "https",
|
|
|
|
HttpOnly: true,
|
|
|
|
Path: "/",
|
|
|
|
SameSite: http.SameSiteLaxMode,
|
|
|
|
}
|
|
|
|
|
|
|
|
http.SetCookie(c.Writer, cookie)
|
2022-02-13 15:49:22 +00:00
|
|
|
|
2022-02-06 12:06:19 +00:00
|
|
|
// redirect back to index
|
2022-02-05 19:59:41 +00:00
|
|
|
c.Redirect(http.StatusSeeOther, "/")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-02-05 16:24:07 +00:00
|
|
|
func (iam *IndieAuthManager) IndieAuthLoginPost(c *gin.Context) {
|
2022-02-05 14:55:57 +00:00
|
|
|
|
|
|
|
err := c.Request.ParseForm()
|
|
|
|
|
2022-02-13 15:49:22 +00:00
|
|
|
if err != nil {
|
2022-02-05 16:24:07 +00:00
|
|
|
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
|
|
|
"message": err,
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
r := c.Request
|
|
|
|
|
|
|
|
profile := r.FormValue("domain")
|
|
|
|
if profile == "" {
|
|
|
|
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
|
|
|
"message": "Empty domain",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
profile = indieauth.CanonicalizeURL(profile)
|
|
|
|
if err := indieauth.IsValidProfileURL(profile); err != nil {
|
|
|
|
err = fmt.Errorf("invalid profile url: %w", err)
|
|
|
|
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
|
|
|
"message": err,
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
i, redirect, err := iam.iac.Authenticate(profile, "")
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
|
|
|
"message": err,
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Printf("profile: %v\n", i)
|
|
|
|
|
|
|
|
err = iam.saveAuthInfo(c.Writer, c.Request, i)
|
|
|
|
if err != nil {
|
|
|
|
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
2022-02-05 14:55:57 +00:00
|
|
|
"message": err,
|
|
|
|
})
|
2022-02-05 16:24:07 +00:00
|
|
|
return
|
2022-02-05 14:55:57 +00:00
|
|
|
}
|
|
|
|
|
2022-02-05 16:24:07 +00:00
|
|
|
// append me param so the user doesn't have to enter this twice
|
2022-02-13 15:49:22 +00:00
|
|
|
redirect = fmt.Sprintf("%v&me=%v", redirect, url.QueryEscape(i.Me))
|
2022-02-05 16:24:07 +00:00
|
|
|
|
|
|
|
c.Redirect(http.StatusSeeOther, redirect)
|
2022-02-05 14:55:57 +00:00
|
|
|
}
|
2022-02-05 16:24:07 +00:00
|
|
|
|
|
|
|
func (iam *IndieAuthManager) LoginCallbackGet(c *gin.Context) {
|
|
|
|
|
|
|
|
config := config.GetConfig()
|
|
|
|
|
|
|
|
i, redirect, err := iam.getInformation(c)
|
|
|
|
if err != nil {
|
|
|
|
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
|
|
|
"message": err,
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
code, err := iam.iac.ValidateCallback(i, c.Request)
|
|
|
|
if err != nil {
|
|
|
|
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
|
|
|
"message": err,
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-02-05 19:59:41 +00:00
|
|
|
// profile, err := iam.iac.FetchProfile(i, code)
|
|
|
|
// if err != nil {
|
|
|
|
// c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
|
|
|
// "message": err,
|
|
|
|
// })
|
|
|
|
// return
|
|
|
|
// }
|
|
|
|
|
|
|
|
token, _, err := iam.iac.GetToken(i, code)
|
2022-02-05 16:24:07 +00:00
|
|
|
if err != nil {
|
|
|
|
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
|
|
|
"message": err,
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-02-05 19:59:41 +00:00
|
|
|
me := token.Extra("me").(string)
|
|
|
|
|
|
|
|
if err := indieauth.IsValidProfileURL(me); err != nil {
|
2022-02-05 16:24:07 +00:00
|
|
|
err = fmt.Errorf("invalid 'me': %w", err)
|
|
|
|
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
|
|
|
"message": err,
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
expiration := time.Now().Add(time.Hour * 24 * 7)
|
|
|
|
|
|
|
|
_, signed, err := iam.jwtAuth.Encode(map[string]interface{}{
|
|
|
|
jwt.SubjectKey: config.GetString("indieauth.sessionSubject"),
|
|
|
|
jwt.IssuedAtKey: time.Now().Unix(),
|
|
|
|
jwt.ExpirationKey: expiration,
|
2022-02-05 19:59:41 +00:00
|
|
|
"user": me,
|
2022-02-13 15:49:22 +00:00
|
|
|
"token": token.AccessToken,
|
2022-02-05 16:24:07 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{
|
|
|
|
"message": err,
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
cookie := &http.Cookie{
|
|
|
|
Name: "jwt",
|
|
|
|
Value: string(signed),
|
|
|
|
Expires: expiration,
|
|
|
|
Secure: c.Request.URL.Scheme == "https",
|
|
|
|
HttpOnly: true,
|
|
|
|
Path: "/",
|
|
|
|
SameSite: http.SameSiteLaxMode,
|
|
|
|
}
|
|
|
|
|
|
|
|
http.SetCookie(c.Writer, cookie)
|
|
|
|
|
|
|
|
if redirect == "" {
|
|
|
|
redirect = "/"
|
|
|
|
}
|
|
|
|
|
|
|
|
c.Redirect(http.StatusSeeOther, redirect)
|
2022-02-13 15:49:22 +00:00
|
|
|
}
|