IndieAuth #25

Merged
h4kor merged 19 commits from auth into master 2022-11-07 19:38:21 +00:00
5 changed files with 165 additions and 20 deletions
Showing only changes of commit ff563c6f05 - Show all commits

View File

@ -339,3 +339,61 @@ func TestAuthRedirectUriSameHost(t *testing.T) {
assertions.AssertStatus(t, rr, http.StatusOK) 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", "", "")
// 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"`
}
var response responseType
json.Unmarshal(rr.Body.Bytes(), &response)
assertions.AssertEqual(t, response.Me, user.FullUrl())
assertions.AssertEqual(t, response.TokenType, "Bearer")
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", "", "")
// 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)
}

View File

@ -171,22 +171,14 @@ func userAuthHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Reque
} }
} }
func userAuthProfileHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) { func verifyAuthCodeRequest(user owl.User, w http.ResponseWriter, r *http.Request) bool {
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 // get form data from post request
err = r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
println("Error parsing form: ", err.Error()) println("Error parsing form: ", err.Error())
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Error parsing form")) w.Write([]byte("Error parsing form"))
return return false
} }
code := r.Form.Get("code") code := r.Form.Get("code")
client_id := r.Form.Get("client_id") client_id := r.Form.Get("client_id")
@ -198,8 +190,21 @@ func userAuthProfileHandler(repo *owl.Repository) func(http.ResponseWriter, *htt
if !valid { if !valid {
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Invalid code")) w.Write([]byte("Invalid code"))
return false
}
return true
}
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 return
} else { }
if verifyAuthCodeRequest(user, w, r) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
type ResponseProfile struct { type ResponseProfile struct {
Name string `json:"name"` Name string `json:"name"`
@ -227,7 +232,48 @@ func userAuthProfileHandler(repo *owl.Repository) func(http.ResponseWriter, *htt
w.Write(jsonData) w.Write(jsonData)
return 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
}
if verifyAuthCodeRequest(user, w, r) {
type Response 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"`
}
accessToken, duration, err := user.GenerateAccessToken()
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,
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
}
} }
} }

View File

@ -17,6 +17,7 @@ func Router(repo *owl.Repository) http.Handler {
router.GET("/user/:user/auth/", userAuthHandler(repo)) router.GET("/user/:user/auth/", userAuthHandler(repo))
router.POST("/user/:user/auth/", userAuthProfileHandler(repo)) router.POST("/user/:user/auth/", userAuthProfileHandler(repo))
router.POST("/user/:user/auth/verify/", userAuthVerifyHandler(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/media/*filepath", userMediaHandler(repo))
router.GET("/user/:user/index.xml", userRSSHandler(repo)) router.GET("/user/:user/index.xml", userRSSHandler(repo))
router.GET("/user/:user/posts/:post/", postHandler(repo)) router.GET("/user/:user/posts/:post/", postHandler(repo))
@ -33,6 +34,7 @@ func SingleUserRouter(repo *owl.Repository) http.Handler {
router.GET("/auth/", userAuthHandler(repo)) router.GET("/auth/", userAuthHandler(repo))
router.POST("/auth/", userAuthProfileHandler(repo)) router.POST("/auth/", userAuthProfileHandler(repo))
router.POST("/auth/verify/", userAuthVerifyHandler(repo)) router.POST("/auth/verify/", userAuthVerifyHandler(repo))
router.POST("/auth/token/", userAuthTokenHandler(repo))
router.GET("/media/*filepath", userMediaHandler(repo)) router.GET("/media/*filepath", userMediaHandler(repo))
router.GET("/index.xml", userRSSHandler(repo)) router.GET("/index.xml", userRSSHandler(repo))
router.GET("/posts/:post/", postHandler(repo)) router.GET("/posts/:post/", postHandler(repo))

View File

@ -29,6 +29,7 @@
<link rel="webmention" href="{{ .User.WebmentionUrl }}"> <link rel="webmention" href="{{ .User.WebmentionUrl }}">
{{ if .User.AuthUrl }} {{ if .User.AuthUrl }}
<link rel="authorization_endpoint" href="{{ .User.AuthUrl}}"> <link rel="authorization_endpoint" href="{{ .User.AuthUrl}}">
<link rel="token_endpoint" href="{{ .User.TokenUrl}}">
{{ end }} {{ end }}
<style> <style>
header { header {

38
user.go
View File

@ -42,6 +42,12 @@ type AuthCode struct {
Created time.Time `yaml:"created"` Created time.Time `yaml:"created"`
} }
type AccessToken struct {
Token string `yaml:"token"`
Created time.Time `yaml:"created"`
ExpiresIn int `yaml:"expires_in"`
}
func (user User) Dir() string { func (user User) Dir() string {
return path.Join(user.repo.UsersDir(), user.name) return path.Join(user.repo.UsersDir(), user.name)
} }
@ -63,6 +69,11 @@ func (user User) AuthUrl() string {
return url return url
} }
func (user User) TokenUrl() string {
url, _ := url.JoinPath(user.AuthUrl(), "token/")
return url
}
func (user User) WebmentionUrl() string { func (user User) WebmentionUrl() string {
url, _ := url.JoinPath(user.FullUrl(), "webmention/") url, _ := url.JoinPath(user.FullUrl(), "webmention/")
return url return url
@ -93,6 +104,10 @@ func (user User) AuthCodesFile() string {
return path.Join(user.MetaDir(), "auth_codes.yml") return path.Join(user.MetaDir(), "auth_codes.yml")
} }
func (user User) AccessTokensFile() string {
return path.Join(user.MetaDir(), "access_tokens.yml")
}
func (user User) Name() string { func (user User) Name() string {
return user.name return user.name
} }
@ -323,3 +338,26 @@ func (user User) VerifyAuthCode(
} }
return false return false
} }
func (user User) getAccessTokens() []AccessToken {
codes := make([]AccessToken, 0)
loadFromYaml(user.AccessTokensFile(), &codes)
return codes
}
func (user User) addAccessToken(code AccessToken) error {
codes := user.getAccessTokens()
codes = append(codes, code)
return saveToYaml(user.AccessTokensFile(), codes)
}
func (user User) GenerateAccessToken() (string, int, error) {
// generate code
token := GenerateRandomString(32)
duration := 24 * 60 * 60
return token, duration, user.addAccessToken(AccessToken{
Token: token,
ExpiresIn: duration,
Created: time.Now(),
})
}