diff --git a/auth_test.go b/auth_test.go
new file mode 100644
index 0000000..5efd35f
--- /dev/null
+++ b/auth_test.go
@@ -0,0 +1,48 @@
+package owl_test
+
+import (
+ "h4kor/owl-blogs"
+ "h4kor/owl-blogs/test/assertions"
+ "net/http"
+ "testing"
+)
+
+func TestGetRedirctUrisLink(t *testing.T) {
+ html := []byte("")
+ parser := &owl.OwlHtmlParser{}
+ uris, err := parser.GetRedirctUris(constructResponse(html))
+
+ assertions.AssertNoError(t, err, "Unable to parse feed")
+
+ assertions.AssertArrayContains(t, uris, "http://example.com/redirect")
+}
+
+func TestGetRedirctUrisLinkMultiple(t *testing.T) {
+ html := []byte(`
+
+
+
+
+
+ `)
+ parser := &owl.OwlHtmlParser{}
+ uris, err := parser.GetRedirctUris(constructResponse(html))
+
+ assertions.AssertNoError(t, err, "Unable to parse feed")
+
+ assertions.AssertArrayContains(t, uris, "http://example.com/redirect1")
+ assertions.AssertArrayContains(t, uris, "http://example.com/redirect2")
+ assertions.AssertArrayContains(t, uris, "http://example.com/redirect3")
+ assertions.AssertLen(t, uris, 3)
+}
+
+func TestGetRedirectUrisLinkHeader(t *testing.T) {
+ html := []byte("")
+ parser := &owl.OwlHtmlParser{}
+ resp := constructResponse(html)
+ resp.Header = http.Header{"Link": []string{"; rel=\"redirect_uri\""}}
+ uris, err := parser.GetRedirctUris(resp)
+
+ assertions.AssertNoError(t, err, "Unable to parse feed")
+ assertions.AssertArrayContains(t, uris, "http://example.com/redirect")
+}
diff --git a/cmd/owl/reset_password.go b/cmd/owl/reset_password.go
index 6c9506c..78179cf 100644
--- a/cmd/owl/reset_password.go
+++ b/cmd/owl/reset_password.go
@@ -3,7 +3,6 @@ package main
import (
"fmt"
"h4kor/owl-blogs"
- "math/rand"
"github.com/spf13/cobra"
)
@@ -35,14 +34,7 @@ var resetPasswordCmd = &cobra.Command{
}
// generate a random password and print it
- const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
-
- b := make([]byte, 16)
- for i := range b {
- b[i] = chars[rand.Intn(len(chars))]
- }
- password := string(b)
-
+ password := owl.GenerateRandomString(16)
user.ResetPassword(password)
fmt.Println("User: ", user.Name())
diff --git a/cmd/owl/web/aliases_test.go b/cmd/owl/web/aliases_test.go
index 58ca579..2c6977b 100644
--- a/cmd/owl/web/aliases_test.go
+++ b/cmd/owl/web/aliases_test.go
@@ -3,7 +3,7 @@ package web_test
import (
"h4kor/owl-blogs"
main "h4kor/owl-blogs/cmd/owl/web"
- "h4kor/owl-blogs/priv/assertions"
+ "h4kor/owl-blogs/test/assertions"
"net/http"
"net/http/httptest"
"os"
diff --git a/cmd/owl/web/auth_test.go b/cmd/owl/web/auth_test.go
new file mode 100644
index 0000000..78ea651
--- /dev/null
+++ b/cmd/owl/web/auth_test.go
@@ -0,0 +1,428 @@
+package web_test
+
+import (
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/json"
+ main "h4kor/owl-blogs/cmd/owl/web"
+ "h4kor/owl-blogs/test/assertions"
+ "h4kor/owl-blogs/test/mocks"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strconv"
+ "strings"
+ "testing"
+)
+
+func TestAuthPostWrongPassword(t *testing.T) {
+ repo, user := getSingleUserTestRepo()
+ user.ResetPassword("testpassword")
+
+ csrfToken := "test_csrf_token"
+
+ // Create Request and Response
+ form := url.Values{}
+ form.Add("password", "wrongpassword")
+ form.Add("client_id", "http://example.com")
+ form.Add("redirect_uri", "http://example.com/response")
+ form.Add("response_type", "code")
+ form.Add("state", "test_state")
+ form.Add("csrf_token", csrfToken)
+
+ req, err := http.NewRequest("POST", user.AuthUrl()+"verify/", strings.NewReader(form.Encode()))
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
+ req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
+ assertions.AssertNoError(t, err, "Error creating request")
+ rr := httptest.NewRecorder()
+ router := main.SingleUserRouter(&repo)
+ router.ServeHTTP(rr, req)
+
+ assertions.AssertStatus(t, rr, http.StatusFound)
+ assertions.AssertContains(t, rr.Header().Get("Location"), "error=invalid_password")
+}
+
+func TestAuthPostCorrectPassword(t *testing.T) {
+ repo, user := getSingleUserTestRepo()
+ user.ResetPassword("testpassword")
+
+ csrfToken := "test_csrf_token"
+
+ // Create Request and Response
+ form := url.Values{}
+ form.Add("password", "testpassword")
+ form.Add("client_id", "http://example.com")
+ form.Add("redirect_uri", "http://example.com/response")
+ form.Add("response_type", "code")
+ form.Add("state", "test_state")
+ form.Add("csrf_token", csrfToken)
+ req, err := http.NewRequest("POST", user.AuthUrl()+"verify/", strings.NewReader(form.Encode()))
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
+ req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
+ assertions.AssertNoError(t, err, "Error creating request")
+ rr := httptest.NewRecorder()
+ router := main.SingleUserRouter(&repo)
+ router.ServeHTTP(rr, req)
+
+ assertions.AssertStatus(t, rr, http.StatusFound)
+ assertions.AssertContains(t, rr.Header().Get("Location"), "code=")
+ assertions.AssertContains(t, rr.Header().Get("Location"), "state=test_state")
+ assertions.AssertContains(t, rr.Header().Get("Location"), "iss="+user.FullUrl())
+ assertions.AssertContains(t, rr.Header().Get("Location"), "http://example.com/response")
+}
+
+func TestAuthPostWithIncorrectCode(t *testing.T) {
+ repo, user := getSingleUserTestRepo()
+ user.ResetPassword("testpassword")
+ user.GenerateAuthCode("http://example.com", "http://example.com/response", "", "", "profile")
+
+ // Create Request and Response
+ form := url.Values{}
+ form.Add("code", "wrongcode")
+ form.Add("client_id", "http://example.com")
+ form.Add("redirect_uri", "http://example.com/response")
+ form.Add("grant_type", "authorization_code")
+ req, err := http.NewRequest("POST", user.AuthUrl(), strings.NewReader(form.Encode()))
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
+ assertions.AssertNoError(t, err, "Error creating request")
+ rr := httptest.NewRecorder()
+ router := main.SingleUserRouter(&repo)
+ router.ServeHTTP(rr, req)
+
+ assertions.AssertStatus(t, rr, http.StatusUnauthorized)
+}
+
+func TestAuthPostWithCorrectCode(t *testing.T) {
+ repo, user := getSingleUserTestRepo()
+ user.ResetPassword("testpassword")
+ code, _ := user.GenerateAuthCode("http://example.com", "http://example.com/response", "", "", "profile")
+
+ // Create Request and Response
+ form := url.Values{}
+ form.Add("code", code)
+ form.Add("client_id", "http://example.com")
+ form.Add("redirect_uri", "http://example.com/response")
+ form.Add("grant_type", "authorization_code")
+ req, err := http.NewRequest("POST", user.AuthUrl(), strings.NewReader(form.Encode()))
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
+ req.Header.Add("Accept", "application/json")
+ assertions.AssertNoError(t, err, "Error creating request")
+ rr := httptest.NewRecorder()
+ router := main.SingleUserRouter(&repo)
+ router.ServeHTTP(rr, req)
+
+ assertions.AssertStatus(t, rr, http.StatusOK)
+ // parse response as json
+ type responseType struct {
+ Me string `json:"me"`
+ }
+ var response responseType
+ json.Unmarshal(rr.Body.Bytes(), &response)
+ assertions.AssertEqual(t, response.Me, user.FullUrl())
+
+}
+
+func TestAuthPostWithCorrectCodeAndPKCE(t *testing.T) {
+ repo, user := getSingleUserTestRepo()
+ user.ResetPassword("testpassword")
+
+ // Create Request and Response
+ code_verifier := "test_code_verifier"
+ // create code challenge
+ h := sha256.New()
+ h.Write([]byte(code_verifier))
+ code_challenge := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
+ code, _ := user.GenerateAuthCode("http://example.com", "http://example.com/response", code_challenge, "S256", "profile")
+
+ form := url.Values{}
+ form.Add("code", code)
+ form.Add("client_id", "http://example.com")
+ form.Add("redirect_uri", "http://example.com/response")
+ form.Add("grant_type", "authorization_code")
+ form.Add("code_verifier", code_verifier)
+ req, err := http.NewRequest("POST", user.AuthUrl(), strings.NewReader(form.Encode()))
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
+ req.Header.Add("Accept", "application/json")
+ assertions.AssertNoError(t, err, "Error creating request")
+ rr := httptest.NewRecorder()
+ router := main.SingleUserRouter(&repo)
+ router.ServeHTTP(rr, req)
+
+ assertions.AssertStatus(t, rr, http.StatusOK)
+ // parse response as json
+ type responseType struct {
+ Me string `json:"me"`
+ }
+ var response responseType
+ json.Unmarshal(rr.Body.Bytes(), &response)
+ assertions.AssertEqual(t, response.Me, user.FullUrl())
+
+}
+
+func TestAuthPostWithCorrectCodeAndWrongPKCE(t *testing.T) {
+ repo, user := getSingleUserTestRepo()
+ user.ResetPassword("testpassword")
+
+ // Create Request and Response
+ code_verifier := "test_code_verifier"
+ // create code challenge
+ h := sha256.New()
+ h.Write([]byte(code_verifier + "wrong"))
+ code_challenge := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
+ code, _ := user.GenerateAuthCode("http://example.com", "http://example.com/response", code_challenge, "S256", "profile")
+
+ form := url.Values{}
+ form.Add("code", code)
+ form.Add("client_id", "http://example.com")
+ form.Add("redirect_uri", "http://example.com/response")
+ form.Add("grant_type", "authorization_code")
+ form.Add("code_verifier", code_verifier)
+ req, err := http.NewRequest("POST", user.AuthUrl(), strings.NewReader(form.Encode()))
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
+ req.Header.Add("Accept", "application/json")
+ assertions.AssertNoError(t, err, "Error creating request")
+ rr := httptest.NewRecorder()
+ router := main.SingleUserRouter(&repo)
+ router.ServeHTTP(rr, req)
+
+ assertions.AssertStatus(t, rr, http.StatusUnauthorized)
+}
+
+func TestAuthPostWithCorrectCodePKCEPlain(t *testing.T) {
+ repo, user := getSingleUserTestRepo()
+ user.ResetPassword("testpassword")
+
+ // Create Request and Response
+ code_verifier := "test_code_verifier"
+ code_challenge := code_verifier
+ code, _ := user.GenerateAuthCode("http://example.com", "http://example.com/response", code_challenge, "plain", "profile")
+
+ form := url.Values{}
+ form.Add("code", code)
+ form.Add("client_id", "http://example.com")
+ form.Add("redirect_uri", "http://example.com/response")
+ form.Add("grant_type", "authorization_code")
+ form.Add("code_verifier", code_verifier)
+ req, err := http.NewRequest("POST", user.AuthUrl(), strings.NewReader(form.Encode()))
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
+ req.Header.Add("Accept", "application/json")
+ assertions.AssertNoError(t, err, "Error creating request")
+ rr := httptest.NewRecorder()
+ router := main.SingleUserRouter(&repo)
+ router.ServeHTTP(rr, req)
+
+ assertions.AssertStatus(t, rr, http.StatusOK)
+}
+
+func TestAuthPostWithCorrectCodePKCEPlainWrong(t *testing.T) {
+ repo, user := getSingleUserTestRepo()
+ user.ResetPassword("testpassword")
+
+ // Create Request and Response
+ code_verifier := "test_code_verifier"
+ code_challenge := code_verifier + "wrong"
+ code, _ := user.GenerateAuthCode("http://example.com", "http://example.com/response", code_challenge, "plain", "profile")
+
+ form := url.Values{}
+ form.Add("code", code)
+ form.Add("client_id", "http://example.com")
+ form.Add("redirect_uri", "http://example.com/response")
+ form.Add("grant_type", "authorization_code")
+ form.Add("code_verifier", code_verifier)
+ req, err := http.NewRequest("POST", user.AuthUrl(), strings.NewReader(form.Encode()))
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
+ req.Header.Add("Accept", "application/json")
+ assertions.AssertNoError(t, err, "Error creating request")
+ rr := httptest.NewRecorder()
+ router := main.SingleUserRouter(&repo)
+ router.ServeHTTP(rr, req)
+
+ assertions.AssertStatus(t, rr, http.StatusUnauthorized)
+}
+
+func TestAuthRedirectUriNotSet(t *testing.T) {
+ repo, user := getSingleUserTestRepo()
+ repo.HttpClient = &mocks.MockHttpClient{}
+ repo.Parser = &mocks.MockParseLinksHtmlParser{
+ Links: []string{"http://example2.com/response"},
+ }
+ user.ResetPassword("testpassword")
+
+ csrfToken := "test_csrf_token"
+
+ // Create Request and Response
+ form := url.Values{}
+ form.Add("password", "wrongpassword")
+ form.Add("client_id", "http://example.com")
+ form.Add("redirect_uri", "http://example2.com/response_not_set")
+ form.Add("response_type", "code")
+ form.Add("state", "test_state")
+ form.Add("csrf_token", csrfToken)
+
+ req, err := http.NewRequest("GET", user.AuthUrl()+"?"+form.Encode(), nil)
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
+ req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
+ assertions.AssertNoError(t, err, "Error creating request")
+ rr := httptest.NewRecorder()
+ router := main.SingleUserRouter(&repo)
+ router.ServeHTTP(rr, req)
+
+ assertions.AssertStatus(t, rr, http.StatusBadRequest)
+}
+
+func TestAuthRedirectUriSet(t *testing.T) {
+ repo, user := getSingleUserTestRepo()
+ repo.HttpClient = &mocks.MockHttpClient{}
+ repo.Parser = &mocks.MockParseLinksHtmlParser{
+ Links: []string{"http://example.com/response"},
+ }
+ user.ResetPassword("testpassword")
+
+ csrfToken := "test_csrf_token"
+
+ // Create Request and Response
+ form := url.Values{}
+ form.Add("password", "wrongpassword")
+ form.Add("client_id", "http://example.com")
+ form.Add("redirect_uri", "http://example.com/response")
+ form.Add("response_type", "code")
+ form.Add("state", "test_state")
+ form.Add("csrf_token", csrfToken)
+
+ req, err := http.NewRequest("GET", user.AuthUrl()+"?"+form.Encode(), nil)
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
+ req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
+ assertions.AssertNoError(t, err, "Error creating request")
+ rr := httptest.NewRecorder()
+ router := main.SingleUserRouter(&repo)
+ router.ServeHTTP(rr, req)
+
+ assertions.AssertStatus(t, rr, http.StatusOK)
+}
+
+func TestAuthRedirectUriSameHost(t *testing.T) {
+ repo, user := getSingleUserTestRepo()
+ repo.HttpClient = &mocks.MockHttpClient{}
+ repo.Parser = &mocks.MockParseLinksHtmlParser{
+ Links: []string{},
+ }
+ user.ResetPassword("testpassword")
+
+ csrfToken := "test_csrf_token"
+
+ // Create Request and Response
+ form := url.Values{}
+ form.Add("password", "wrongpassword")
+ form.Add("client_id", "http://example.com")
+ form.Add("redirect_uri", "http://example.com/response")
+ form.Add("response_type", "code")
+ form.Add("state", "test_state")
+ form.Add("csrf_token", csrfToken)
+
+ req, err := http.NewRequest("GET", user.AuthUrl()+"?"+form.Encode(), nil)
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
+ req.AddCookie(&http.Cookie{Name: "csrf_token", Value: csrfToken})
+ assertions.AssertNoError(t, err, "Error creating request")
+ rr := httptest.NewRecorder()
+ router := main.SingleUserRouter(&repo)
+ router.ServeHTTP(rr, req)
+
+ assertions.AssertStatus(t, rr, http.StatusOK)
+}
+
+func TestAccessTokenCorrectPassword(t *testing.T) {
+ repo, user := getSingleUserTestRepo()
+ user.ResetPassword("testpassword")
+ code, _ := user.GenerateAuthCode("http://example.com", "http://example.com/response", "", "", "profile create")
+
+ // Create Request and Response
+ form := url.Values{}
+ form.Add("code", code)
+ form.Add("client_id", "http://example.com")
+ form.Add("redirect_uri", "http://example.com/response")
+ form.Add("grant_type", "authorization_code")
+ req, err := http.NewRequest("POST", user.AuthUrl()+"token/", strings.NewReader(form.Encode()))
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
+ assertions.AssertNoError(t, err, "Error creating request")
+ rr := httptest.NewRecorder()
+ router := main.SingleUserRouter(&repo)
+ router.ServeHTTP(rr, req)
+
+ assertions.AssertStatus(t, rr, http.StatusOK)
+ // parse response as json
+ type responseType struct {
+ Me string `json:"me"`
+ TokenType string `json:"token_type"`
+ AccessToken string `json:"access_token"`
+ ExpiresIn int `json:"expires_in"`
+ RefreshToken string `json:"refresh_token"`
+ Scope string `json:"scope"`
+ }
+ var response responseType
+ json.Unmarshal(rr.Body.Bytes(), &response)
+ assertions.AssertEqual(t, response.Me, user.FullUrl())
+ assertions.AssertEqual(t, response.TokenType, "Bearer")
+ assertions.AssertEqual(t, response.Scope, "profile create")
+ assertions.Assert(t, response.ExpiresIn > 0, "ExpiresIn should be greater than 0")
+ assertions.Assert(t, len(response.AccessToken) > 0, "AccessToken should be greater than 0")
+}
+
+func TestAccessTokenWithIncorrectCode(t *testing.T) {
+ repo, user := getSingleUserTestRepo()
+ user.ResetPassword("testpassword")
+ user.GenerateAuthCode("http://example.com", "http://example.com/response", "", "", "profile")
+
+ // Create Request and Response
+ form := url.Values{}
+ form.Add("code", "wrongcode")
+ form.Add("client_id", "http://example.com")
+ form.Add("redirect_uri", "http://example.com/response")
+ form.Add("grant_type", "authorization_code")
+ req, err := http.NewRequest("POST", user.AuthUrl()+"token/", strings.NewReader(form.Encode()))
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
+ assertions.AssertNoError(t, err, "Error creating request")
+ rr := httptest.NewRecorder()
+ router := main.SingleUserRouter(&repo)
+ router.ServeHTTP(rr, req)
+
+ assertions.AssertStatus(t, rr, http.StatusUnauthorized)
+}
+
+func TestIndieauthMetadata(t *testing.T) {
+ repo, user := getSingleUserTestRepo()
+ user.ResetPassword("testpassword")
+ req, _ := http.NewRequest("GET", user.IndieauthMetadataUrl(), nil)
+ rr := httptest.NewRecorder()
+ router := main.SingleUserRouter(&repo)
+ router.ServeHTTP(rr, req)
+
+ assertions.AssertStatus(t, rr, http.StatusOK)
+ // parse response as json
+ type responseType struct {
+ Issuer string `json:"issuer"`
+ AuthorizationEndpoint string `json:"authorization_endpoint"`
+ TokenEndpoint string `json:"token_endpoint"`
+ CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
+ ScopesSupported []string `json:"scopes_supported"`
+ ResponseTypesSupported []string `json:"response_types_supported"`
+ GrantTypesSupported []string `json:"grant_types_supported"`
+ }
+ var response responseType
+ json.Unmarshal(rr.Body.Bytes(), &response)
+ assertions.AssertEqual(t, response.Issuer, user.FullUrl())
+ assertions.AssertEqual(t, response.AuthorizationEndpoint, user.AuthUrl())
+ assertions.AssertEqual(t, response.TokenEndpoint, user.TokenUrl())
+}
diff --git a/cmd/owl/web/handler.go b/cmd/owl/web/handler.go
index c835c4c..f9e5616 100644
--- a/cmd/owl/web/handler.go
+++ b/cmd/owl/web/handler.go
@@ -1,6 +1,7 @@
package web
import (
+ "encoding/json"
"fmt"
"h4kor/owl-blogs"
"net/http"
@@ -59,6 +60,352 @@ func userIndexHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Requ
}
}
+func userAuthMetadataHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
+ return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
+ user, err := getUserFromRepo(repo, ps)
+ if err != nil {
+ println("Error getting user: ", err.Error())
+ notFoundHandler(repo)(w, r)
+ return
+ }
+
+ type Response struct {
+ Issuer string `json:"issuer"`
+ AuthorizationEndpoint string `json:"authorization_endpoint"`
+ TokenEndpoint string `json:"token_endpoint"`
+ CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
+ ScopesSupported []string `json:"scopes_supported"`
+ ResponseTypesSupported []string `json:"response_types_supported"`
+ GrantTypesSupported []string `json:"grant_types_supported"`
+ }
+ response := Response{
+ Issuer: user.FullUrl(),
+ AuthorizationEndpoint: user.AuthUrl(),
+ TokenEndpoint: user.TokenUrl(),
+ CodeChallengeMethodsSupported: []string{"S256", "plain"},
+ ScopesSupported: []string{"profile"},
+ ResponseTypesSupported: []string{"code"},
+ GrantTypesSupported: []string{"authorization_code"},
+ }
+ jsonData, err := json.Marshal(response)
+ if err != nil {
+ println("Error marshalling json: ", err.Error())
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte("Internal server error"))
+ }
+ w.Write(jsonData)
+ }
+}
+
+func userAuthHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
+ return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
+ user, err := getUserFromRepo(repo, ps)
+ if err != nil {
+ println("Error getting user: ", err.Error())
+ notFoundHandler(repo)(w, r)
+ return
+ }
+ // get me, cleint_id, redirect_uri, state and response_type from query
+ me := r.URL.Query().Get("me")
+ clientId := r.URL.Query().Get("client_id")
+ redirectUri := r.URL.Query().Get("redirect_uri")
+ state := r.URL.Query().Get("state")
+ responseType := r.URL.Query().Get("response_type")
+ codeChallenge := r.URL.Query().Get("code_challenge")
+ codeChallengeMethod := r.URL.Query().Get("code_challenge_method")
+ scope := r.URL.Query().Get("scope")
+
+ // check if request is valid
+ missing_params := []string{}
+ if clientId == "" {
+ missing_params = append(missing_params, "client_id")
+ }
+ if redirectUri == "" {
+ missing_params = append(missing_params, "redirect_uri")
+ }
+ if responseType == "" {
+ missing_params = append(missing_params, "response_type")
+ }
+ if len(missing_params) > 0 {
+ w.WriteHeader(http.StatusBadRequest)
+ w.Write([]byte(fmt.Sprintf("Missing parameters: %s", strings.Join(missing_params, ", "))))
+ return
+ }
+ if responseType != "id" {
+ responseType = "code"
+ }
+ if responseType != "code" {
+ w.WriteHeader(http.StatusBadRequest)
+ w.Write([]byte("Invalid response_type. Must be 'code' ('id' converted to 'code' for legacy support)."))
+ return
+ }
+ if codeChallengeMethod != "" && (codeChallengeMethod != "S256" && codeChallengeMethod != "plain") {
+ w.WriteHeader(http.StatusBadRequest)
+ w.Write([]byte("Invalid code_challenge_method. Must be 'S256' or 'plain'."))
+ return
+ }
+
+ client_id_url, err := url.Parse(clientId)
+ if err != nil {
+ w.WriteHeader(http.StatusBadRequest)
+ w.Write([]byte("Invalid client_id."))
+ return
+ }
+ redirect_uri_url, err := url.Parse(redirectUri)
+ if err != nil {
+ w.WriteHeader(http.StatusBadRequest)
+ w.Write([]byte("Invalid redirect_uri."))
+ return
+ }
+ if client_id_url.Host != redirect_uri_url.Host || client_id_url.Scheme != redirect_uri_url.Scheme {
+ // check if redirect_uri is registered
+ resp, _ := repo.HttpClient.Get(clientId)
+ registered_redirects, _ := repo.Parser.GetRedirctUris(resp)
+ is_registered := false
+ for _, registered_redirect := range registered_redirects {
+ if registered_redirect == redirectUri {
+ // redirect_uri is registered
+ is_registered = true
+ break
+ }
+ }
+ if !is_registered {
+ w.WriteHeader(http.StatusBadRequest)
+ w.Write([]byte("Invalid redirect_uri. Must be registered with client_id."))
+ return
+ }
+ }
+
+ // Double Submit Cookie Pattern
+ // https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie
+ csrfToken := owl.GenerateRandomString(32)
+ cookie := http.Cookie{
+ Name: "csrf_token",
+ Value: csrfToken,
+ }
+ http.SetCookie(w, &cookie)
+
+ reqData := owl.AuthRequestData{
+ Me: me,
+ ClientId: clientId,
+ RedirectUri: redirectUri,
+ State: state,
+ Scope: scope,
+ ResponseType: responseType,
+ CodeChallenge: codeChallenge,
+ CodeChallengeMethod: codeChallengeMethod,
+ User: user,
+ CsrfToken: csrfToken,
+ }
+
+ html, err := owl.RenderUserAuthPage(reqData)
+ if err != nil {
+ println("Error rendering auth page: ", err.Error())
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte("Internal server error"))
+ return
+ }
+ println("Rendering auth page for user", user.Name())
+ w.Write([]byte(html))
+ }
+}
+
+func verifyAuthCodeRequest(user owl.User, w http.ResponseWriter, r *http.Request) (bool, owl.AuthCode) {
+ // get form data from post request
+ err := r.ParseForm()
+ if err != nil {
+ println("Error parsing form: ", err.Error())
+ w.WriteHeader(http.StatusBadRequest)
+ w.Write([]byte("Error parsing form"))
+ return false, owl.AuthCode{}
+ }
+ code := r.Form.Get("code")
+ client_id := r.Form.Get("client_id")
+ redirect_uri := r.Form.Get("redirect_uri")
+ code_verifier := r.Form.Get("code_verifier")
+
+ // check if request is valid
+ valid, authCode := user.VerifyAuthCode(code, client_id, redirect_uri, code_verifier)
+ if !valid {
+ w.WriteHeader(http.StatusUnauthorized)
+ w.Write([]byte("Invalid code"))
+ }
+ return valid, authCode
+}
+
+func userAuthProfileHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
+ return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
+ user, err := getUserFromRepo(repo, ps)
+ if err != nil {
+ println("Error getting user: ", err.Error())
+ notFoundHandler(repo)(w, r)
+ return
+ }
+
+ valid, _ := verifyAuthCodeRequest(user, w, r)
+ if valid {
+ w.WriteHeader(http.StatusOK)
+ type ResponseProfile struct {
+ Name string `json:"name"`
+ Url string `json:"url"`
+ Photo string `json:"photo"`
+ }
+ type Response struct {
+ Me string `json:"me"`
+ Profile ResponseProfile `json:"profile"`
+ }
+ response := Response{
+ Me: user.FullUrl(),
+ Profile: ResponseProfile{
+ Name: user.Name(),
+ Url: user.FullUrl(),
+ Photo: user.AvatarUrl(),
+ },
+ }
+ jsonData, err := json.Marshal(response)
+ if err != nil {
+ println("Error marshalling json: ", err.Error())
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte("Internal server error"))
+ }
+ w.Write(jsonData)
+ return
+ }
+ }
+}
+
+func userAuthTokenHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
+ return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
+ user, err := getUserFromRepo(repo, ps)
+ if err != nil {
+ println("Error getting user: ", err.Error())
+ notFoundHandler(repo)(w, r)
+ return
+ }
+
+ valid, authCode := verifyAuthCodeRequest(user, w, r)
+ if valid {
+ if authCode.Scope == "" {
+ w.WriteHeader(http.StatusBadRequest)
+ w.Write([]byte("Empty scope, no token issued"))
+ return
+ }
+
+ type Response struct {
+ Me string `json:"me"`
+ TokenType string `json:"token_type"`
+ AccessToken string `json:"access_token"`
+ Scope string `json:"scope"`
+ ExpiresIn int `json:"expires_in"`
+ RefreshToken string `json:"refresh_token"`
+ }
+ accessToken, duration, err := user.GenerateAccessToken(authCode)
+ if err != nil {
+ println("Error generating access token: ", err.Error())
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte("Internal server error"))
+ return
+ }
+ response := Response{
+ Me: user.FullUrl(),
+ TokenType: "Bearer",
+ AccessToken: accessToken,
+ Scope: authCode.Scope,
+ ExpiresIn: duration,
+ }
+ jsonData, err := json.Marshal(response)
+ if err != nil {
+ println("Error marshalling json: ", err.Error())
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte("Internal server error"))
+ }
+ w.Write(jsonData)
+ return
+ }
+ }
+}
+
+func userAuthVerifyHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
+ return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
+ user, err := getUserFromRepo(repo, ps)
+ if err != nil {
+ println("Error getting user: ", err.Error())
+ notFoundHandler(repo)(w, r)
+ return
+ }
+
+ // get form data from post request
+ err = r.ParseForm()
+ if err != nil {
+ println("Error parsing form: ", err.Error())
+ w.WriteHeader(http.StatusBadRequest)
+ w.Write([]byte("Error parsing form"))
+ return
+ }
+ password := r.FormValue("password")
+ client_id := r.FormValue("client_id")
+ redirect_uri := r.FormValue("redirect_uri")
+ response_type := r.FormValue("response_type")
+ state := r.FormValue("state")
+ code_challenge := r.FormValue("code_challenge")
+ code_challenge_method := r.FormValue("code_challenge_method")
+ scope := r.FormValue("scope")
+
+ // CSRF check
+ formCsrfToken := r.FormValue("csrf_token")
+ cookieCsrfToken, err := r.Cookie("csrf_token")
+
+ if err != nil {
+ println("Error getting csrf token from cookie: ", err.Error())
+ w.WriteHeader(http.StatusBadRequest)
+ w.Write([]byte("Error getting csrf token from cookie"))
+ return
+ }
+ if formCsrfToken != cookieCsrfToken.Value {
+ println("Invalid csrf token")
+ w.WriteHeader(http.StatusBadRequest)
+ w.Write([]byte("Invalid csrf token"))
+ return
+ }
+
+ password_valid := user.VerifyPassword(password)
+ if !password_valid {
+ redirect := fmt.Sprintf(
+ "%s?error=invalid_password&client_id=%s&redirect_uri=%s&response_type=%s&state=%s",
+ user.AuthUrl(), client_id, redirect_uri, response_type, state,
+ )
+ if code_challenge != "" {
+ redirect += fmt.Sprintf("&code_challenge=%s&code_challenge_method=%s", code_challenge, code_challenge_method)
+ }
+ http.Redirect(w, r,
+ redirect,
+ http.StatusFound,
+ )
+ return
+ } else {
+ // password is valid, generate code
+ code, err := user.GenerateAuthCode(
+ client_id, redirect_uri, code_challenge, code_challenge_method, scope)
+ if err != nil {
+ println("Error generating code: ", err.Error())
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte("Internal server error"))
+ return
+ }
+ http.Redirect(w, r,
+ fmt.Sprintf(
+ "%s?code=%s&state=%s&iss=%s",
+ redirect_uri, code, state,
+ user.FullUrl(),
+ ),
+ http.StatusFound,
+ )
+ return
+ }
+
+ }
+}
+
func userWebmentionHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
user, err := getUserFromRepo(repo, ps)
diff --git a/cmd/owl/web/multi_user_test.go b/cmd/owl/web/multi_user_test.go
index 62382ed..1772596 100644
--- a/cmd/owl/web/multi_user_test.go
+++ b/cmd/owl/web/multi_user_test.go
@@ -3,7 +3,7 @@ package web_test
import (
"h4kor/owl-blogs"
main "h4kor/owl-blogs/cmd/owl/web"
- "h4kor/owl-blogs/priv/assertions"
+ "h4kor/owl-blogs/test/assertions"
"math/rand"
"net/http"
"net/http/httptest"
diff --git a/cmd/owl/web/post_test.go b/cmd/owl/web/post_test.go
index 2d7a6c9..f593588 100644
--- a/cmd/owl/web/post_test.go
+++ b/cmd/owl/web/post_test.go
@@ -3,7 +3,7 @@ package web_test
import (
"h4kor/owl-blogs"
main "h4kor/owl-blogs/cmd/owl/web"
- "h4kor/owl-blogs/priv/assertions"
+ "h4kor/owl-blogs/test/assertions"
"net/http"
"net/http/httptest"
"os"
diff --git a/cmd/owl/web/rss_test.go b/cmd/owl/web/rss_test.go
index 2877100..20b5fa4 100644
--- a/cmd/owl/web/rss_test.go
+++ b/cmd/owl/web/rss_test.go
@@ -3,7 +3,7 @@ package web_test
import (
"h4kor/owl-blogs"
main "h4kor/owl-blogs/cmd/owl/web"
- "h4kor/owl-blogs/priv/assertions"
+ "h4kor/owl-blogs/test/assertions"
"net/http"
"net/http/httptest"
"testing"
diff --git a/cmd/owl/web/server.go b/cmd/owl/web/server.go
index 81d3d3b..4967735 100644
--- a/cmd/owl/web/server.go
+++ b/cmd/owl/web/server.go
@@ -14,11 +14,16 @@ func Router(repo *owl.Repository) http.Handler {
router.ServeFiles("/static/*filepath", http.Dir(repo.StaticDir()))
router.GET("/", repoIndexHandler(repo))
router.GET("/user/:user/", userIndexHandler(repo))
+ router.GET("/user/:user/auth/", userAuthHandler(repo))
+ router.POST("/user/:user/auth/", userAuthProfileHandler(repo))
+ router.POST("/user/:user/auth/verify/", userAuthVerifyHandler(repo))
+ router.POST("/user/:user/auth/token/", userAuthTokenHandler(repo))
router.GET("/user/:user/media/*filepath", userMediaHandler(repo))
router.GET("/user/:user/index.xml", userRSSHandler(repo))
router.GET("/user/:user/posts/:post/", postHandler(repo))
router.GET("/user/:user/posts/:post/media/*filepath", postMediaHandler(repo))
router.POST("/user/:user/webmention/", userWebmentionHandler(repo))
+ router.GET("/user/:user/.well-known/oauth-authorization-server", userAuthMetadataHandler(repo))
router.NotFound = http.HandlerFunc(notFoundHandler(repo))
return router
}
@@ -27,11 +32,16 @@ func SingleUserRouter(repo *owl.Repository) http.Handler {
router := httprouter.New()
router.ServeFiles("/static/*filepath", http.Dir(repo.StaticDir()))
router.GET("/", userIndexHandler(repo))
+ router.GET("/auth/", userAuthHandler(repo))
+ router.POST("/auth/", userAuthProfileHandler(repo))
+ router.POST("/auth/verify/", userAuthVerifyHandler(repo))
+ router.POST("/auth/token/", userAuthTokenHandler(repo))
router.GET("/media/*filepath", userMediaHandler(repo))
router.GET("/index.xml", userRSSHandler(repo))
router.GET("/posts/:post/", postHandler(repo))
router.GET("/posts/:post/media/*filepath", postMediaHandler(repo))
router.POST("/webmention/", userWebmentionHandler(repo))
+ router.GET("/.well-known/oauth-authorization-server", userAuthMetadataHandler(repo))
router.NotFound = http.HandlerFunc(notFoundHandler(repo))
return router
}
diff --git a/cmd/owl/web/single_user_test.go b/cmd/owl/web/single_user_test.go
index 069b3ec..2ffbc9e 100644
--- a/cmd/owl/web/single_user_test.go
+++ b/cmd/owl/web/single_user_test.go
@@ -3,7 +3,7 @@ package web_test
import (
owl "h4kor/owl-blogs"
main "h4kor/owl-blogs/cmd/owl/web"
- "h4kor/owl-blogs/priv/assertions"
+ "h4kor/owl-blogs/test/assertions"
"net/http"
"net/http/httptest"
"os"
diff --git a/cmd/owl/web/webmention_test.go b/cmd/owl/web/webmention_test.go
index e2c3105..6806149 100644
--- a/cmd/owl/web/webmention_test.go
+++ b/cmd/owl/web/webmention_test.go
@@ -3,7 +3,7 @@ package web_test
import (
"h4kor/owl-blogs"
main "h4kor/owl-blogs/cmd/owl/web"
- "h4kor/owl-blogs/priv/assertions"
+ "h4kor/owl-blogs/test/assertions"
"net/http"
"net/http/httptest"
"net/url"
diff --git a/embed/auth.html b/embed/auth.html
new file mode 100644
index 0000000..1e4eec1
--- /dev/null
+++ b/embed/auth.html
@@ -0,0 +1,24 @@
+Authorization for {{.ClientId}}
+
+Requesting scope:
+
+ {{range $index, $element := .Scopes}}
+ - {{$element}}
+ {{end}}
+
+
+
+
+
\ No newline at end of file
diff --git a/embed/initial/base.html b/embed/initial/base.html
index 2c2493b..78ad250 100644
--- a/embed/initial/base.html
+++ b/embed/initial/base.html
@@ -27,6 +27,11 @@
+ {{ if .User.AuthUrl }}
+
+
+
+ {{ end }}