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:
+ + +

+ +
+ + + + + + + + + + + +
\ 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 }}