2024-03-27 17:02:31 -04:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/sha256"
|
|
|
|
"database/sql"
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/json"
|
2024-03-28 12:57:35 -04:00
|
|
|
"fmt"
|
2024-03-27 17:02:31 -04:00
|
|
|
"io"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/database"
|
|
|
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/utils"
|
|
|
|
"golang.org/x/oauth2"
|
|
|
|
)
|
|
|
|
|
|
|
|
func StartSessionContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
|
|
|
|
return func(success Continuation, failure Continuation) ContinuationChain {
|
|
|
|
verifier := utils.RandomId() + utils.RandomId()
|
|
|
|
|
|
|
|
sha2 := sha256.New()
|
|
|
|
io.WriteString(sha2, verifier)
|
|
|
|
codeChallenge := base64.RawURLEncoding.EncodeToString(sha2.Sum(nil))
|
|
|
|
|
|
|
|
state := utils.RandomId()
|
|
|
|
url := context.Args.OauthConfig.AuthCodeURL(state, oauth2.SetAuthURLParam("code_challenge_method", "S256"), oauth2.SetAuthURLParam("code_challenge", codeChallenge))
|
|
|
|
|
|
|
|
http.SetCookie(resp, &http.Cookie{
|
|
|
|
Name: "verifier",
|
|
|
|
Value: verifier,
|
|
|
|
Path: "/",
|
|
|
|
Secure: true,
|
|
|
|
SameSite: http.SameSiteLaxMode,
|
|
|
|
MaxAge: 60,
|
|
|
|
})
|
|
|
|
http.SetCookie(resp, &http.Cookie{
|
|
|
|
Name: "state",
|
|
|
|
Value: state,
|
|
|
|
Path: "/",
|
|
|
|
Secure: true,
|
|
|
|
SameSite: http.SameSiteLaxMode,
|
|
|
|
MaxAge: 60,
|
|
|
|
})
|
|
|
|
|
|
|
|
http.Redirect(resp, req, url, http.StatusFound)
|
|
|
|
return success(context, req, resp)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-03 17:58:44 -04:00
|
|
|
func InterceptOauthCodeContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
|
2024-03-27 17:02:31 -04:00
|
|
|
return func(success Continuation, failure Continuation) ContinuationChain {
|
|
|
|
state := req.URL.Query().Get("state")
|
|
|
|
code := req.URL.Query().Get("code")
|
|
|
|
|
|
|
|
if code == "" || state == "" {
|
|
|
|
resp.WriteHeader(http.StatusBadRequest)
|
|
|
|
return failure(context, req, resp)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !verifyState(req, "state", state) {
|
|
|
|
resp.WriteHeader(http.StatusBadRequest)
|
|
|
|
return failure(context, req, resp)
|
|
|
|
}
|
|
|
|
verifierCookie, err := req.Cookie("verifier")
|
|
|
|
if err != nil {
|
|
|
|
resp.WriteHeader(http.StatusBadRequest)
|
|
|
|
return failure(context, req, resp)
|
|
|
|
}
|
|
|
|
|
|
|
|
reqContext := req.Context()
|
|
|
|
token, err := context.Args.OauthConfig.Exchange(reqContext, code, oauth2.SetAuthURLParam("code_verifier", verifierCookie.Value))
|
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
resp.WriteHeader(http.StatusInternalServerError)
|
|
|
|
return failure(context, req, resp)
|
|
|
|
}
|
|
|
|
|
|
|
|
client := context.Args.OauthConfig.Client(reqContext, token)
|
|
|
|
user, err := getOauthUser(context.DBConn, client, context.Args.OauthUserInfoURI)
|
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
resp.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
|
|
|
return failure(context, req, resp)
|
|
|
|
}
|
|
|
|
|
|
|
|
session, err := database.MakeUserSessionFor(context.DBConn, user)
|
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
resp.WriteHeader(http.StatusInternalServerError)
|
|
|
|
return failure(context, req, resp)
|
|
|
|
}
|
|
|
|
|
|
|
|
http.SetCookie(resp, &http.Cookie{
|
|
|
|
Name: "session",
|
|
|
|
Value: session.ID,
|
|
|
|
Path: "/",
|
|
|
|
SameSite: http.SameSiteLaxMode,
|
|
|
|
Secure: true,
|
|
|
|
})
|
|
|
|
|
|
|
|
redirect := "/"
|
|
|
|
redirectCookie, err := req.Cookie("redirect")
|
|
|
|
if err == nil && redirectCookie.Value != "" {
|
|
|
|
redirect = redirectCookie.Value
|
|
|
|
http.SetCookie(resp, &http.Cookie{
|
|
|
|
Name: "redirect",
|
|
|
|
MaxAge: 0,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
http.Redirect(resp, req, redirect, http.StatusFound)
|
|
|
|
return success(context, req, resp)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-28 12:57:35 -04:00
|
|
|
func getUserFromAuthHeader(dbConn *sql.DB, bearerToken string) (*database.User, error) {
|
|
|
|
if bearerToken == "" {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
parts := strings.Split(bearerToken, " ")
|
|
|
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
apiKey, err := database.GetAPIKey(dbConn, parts[1])
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if apiKey == nil {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
user, err := database.GetUser(dbConn, apiKey.UserID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return user, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func getUserFromSession(dbConn *sql.DB, sessionId string) (*database.User, error) {
|
|
|
|
session, err := database.GetSession(dbConn, sessionId)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if session.ExpireAt.Before(time.Now()) {
|
|
|
|
session = nil
|
|
|
|
database.DeleteSession(dbConn, sessionId)
|
|
|
|
return nil, fmt.Errorf("session expired")
|
|
|
|
}
|
|
|
|
|
|
|
|
user, err := database.GetUser(dbConn, session.UserID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return user, nil
|
|
|
|
}
|
|
|
|
|
2024-03-27 17:02:31 -04:00
|
|
|
func VerifySessionContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
|
|
|
|
return func(success Continuation, failure Continuation) ContinuationChain {
|
2024-03-28 12:57:35 -04:00
|
|
|
authHeader := req.Header.Get("Authorization")
|
|
|
|
user, userErr := getUserFromAuthHeader(context.DBConn, authHeader)
|
|
|
|
|
2024-03-27 17:02:31 -04:00
|
|
|
sessionCookie, err := req.Cookie("session")
|
2024-03-28 13:06:31 -04:00
|
|
|
if err == nil && sessionCookie.Value != "" {
|
2024-03-28 12:57:35 -04:00
|
|
|
user, userErr = getUserFromSession(context.DBConn, sessionCookie.Value)
|
2024-03-27 17:02:31 -04:00
|
|
|
}
|
|
|
|
|
2024-03-28 12:57:35 -04:00
|
|
|
if userErr != nil || user == nil {
|
|
|
|
log.Println(userErr, user)
|
|
|
|
|
2024-03-27 17:02:31 -04:00
|
|
|
http.SetCookie(resp, &http.Cookie{
|
|
|
|
Name: "session",
|
2024-03-28 12:57:35 -04:00
|
|
|
MaxAge: 0, // reset session cookie in case
|
2024-03-27 17:02:31 -04:00
|
|
|
})
|
2024-03-28 13:06:31 -04:00
|
|
|
|
|
|
|
context.User = nil
|
2024-03-27 17:02:31 -04:00
|
|
|
return failure(context, req, resp)
|
|
|
|
}
|
|
|
|
|
|
|
|
context.User = user
|
|
|
|
return success(context, req, resp)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func GoLoginContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
|
|
|
|
return func(success Continuation, failure Continuation) ContinuationChain {
|
|
|
|
http.SetCookie(resp, &http.Cookie{
|
|
|
|
Name: "redirect",
|
|
|
|
Value: req.URL.Path,
|
|
|
|
Path: "/",
|
|
|
|
Secure: true,
|
|
|
|
SameSite: http.SameSiteLaxMode,
|
|
|
|
})
|
|
|
|
|
|
|
|
http.Redirect(resp, req, "/login", http.StatusFound)
|
|
|
|
return failure(context, req, resp)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func RefreshSessionContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
|
|
|
|
return func(success Continuation, failure Continuation) ContinuationChain {
|
|
|
|
sessionCookie, err := req.Cookie("session")
|
|
|
|
if err != nil {
|
|
|
|
resp.WriteHeader(http.StatusUnauthorized)
|
|
|
|
return failure(context, req, resp)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = database.RefreshSession(context.DBConn, sessionCookie.Value)
|
|
|
|
if err != nil {
|
|
|
|
resp.WriteHeader(http.StatusUnauthorized)
|
|
|
|
return failure(context, req, resp)
|
|
|
|
}
|
|
|
|
|
|
|
|
return success(context, req, resp)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func LogoutContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
|
|
|
|
return func(success Continuation, failure Continuation) ContinuationChain {
|
|
|
|
sessionCookie, err := req.Cookie("session")
|
|
|
|
if err == nil && sessionCookie.Value != "" {
|
|
|
|
_ = database.DeleteSession(context.DBConn, sessionCookie.Value)
|
|
|
|
}
|
|
|
|
|
|
|
|
http.Redirect(resp, req, "/", http.StatusFound)
|
|
|
|
http.SetCookie(resp, &http.Cookie{
|
|
|
|
Name: "session",
|
|
|
|
MaxAge: 0,
|
|
|
|
})
|
|
|
|
return success(context, req, resp)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func getOauthUser(dbConn *sql.DB, client *http.Client, uri string) (*database.User, error) {
|
|
|
|
userResponse, err := client.Get(uri)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
userStruct, err := createUserFromResponse(userResponse)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
user, err := database.FindOrSaveUser(dbConn, userStruct)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return user, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func createUserFromResponse(response *http.Response) (*database.User, error) {
|
|
|
|
user := &database.User{
|
|
|
|
CreatedAt: time.Now(),
|
|
|
|
}
|
2024-04-02 16:53:50 -04:00
|
|
|
|
2024-03-27 17:02:31 -04:00
|
|
|
err := json.NewDecoder(response.Body).Decode(user)
|
2024-04-02 16:53:50 -04:00
|
|
|
defer response.Body.Close()
|
|
|
|
|
2024-03-27 17:02:31 -04:00
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
user.Username = strings.ToLower(user.Username)
|
|
|
|
user.Username = strings.Split(user.Username, "@")[0]
|
|
|
|
|
|
|
|
return user, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func verifyState(req *http.Request, stateCookieName string, expectedState string) bool {
|
|
|
|
cookie, err := req.Cookie(stateCookieName)
|
|
|
|
if err != nil || cookie.Value != expectedState {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|