refactored common oauth 2.0 provider and add unit tests for nextcloud oauth 2.0 data source
This commit is contained in:
@@ -0,0 +1,78 @@
|
|||||||
|
package oauth2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommonOAuth2Provider represents common OAuth 2.0 provider
|
||||||
|
type CommonOAuth2Provider struct {
|
||||||
|
OAuth2Provider
|
||||||
|
dataSource CommonOAuth2DataSource
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommonOAuth2DataSource defines the structure of OAuth 2.0 data source
|
||||||
|
type CommonOAuth2DataSource interface {
|
||||||
|
// GetAuthUrl returns the authentication url of the data source
|
||||||
|
GetAuthUrl() string
|
||||||
|
|
||||||
|
// GetTokenUrl returns the token url of the data source
|
||||||
|
GetTokenUrl() string
|
||||||
|
|
||||||
|
// GetUserInfoRequest returns the user info request of the data source
|
||||||
|
GetUserInfoRequest() (*http.Request, error)
|
||||||
|
|
||||||
|
// GetScopes returns the scopes required by the data source
|
||||||
|
GetScopes() []string
|
||||||
|
|
||||||
|
// ParseUserInfo returns the user info by parsing the response body
|
||||||
|
ParseUserInfo(c core.Context, body []byte) (*OAuth2UserInfo, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAuthUrl returns the authentication url of the common OAuth 2.0 provider
|
||||||
|
func (p *CommonOAuth2Provider) GetAuthUrl() string {
|
||||||
|
return p.dataSource.GetAuthUrl()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTokenUrl returns the token url of the common OAuth 2.0 provider
|
||||||
|
func (p *CommonOAuth2Provider) GetTokenUrl() string {
|
||||||
|
return p.dataSource.GetTokenUrl()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScopes returns the scopes required by the common OAuth 2.0 provider
|
||||||
|
func (p *CommonOAuth2Provider) GetScopes() []string {
|
||||||
|
return p.dataSource.GetScopes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfo returns the user info by the common OAuth 2.0 provider
|
||||||
|
func (p *CommonOAuth2Provider) GetUserInfo(c core.Context, oauth2Client *http.Client) (*OAuth2UserInfo, error) {
|
||||||
|
req, err := p.dataSource.GetUserInfoRequest()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[common_oauth2_provider.GetUserInfo] failed to get user info request, because %s", err.Error())
|
||||||
|
return nil, errs.ErrFailedToRequestRemoteApi
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := oauth2Client.Do(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(c, "[common_oauth2_provider.GetUserInfo] failed to get user info response, because %s", err.Error())
|
||||||
|
return nil, errs.ErrFailedToRequestRemoteApi
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
log.Debugf(c, "[common_oauth2_provider.GetUserInfo] response is %s", body)
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
log.Errorf(c, "[common_oauth2_provider.GetUserInfo] failed to get user info response, because response code is %d", resp.StatusCode)
|
||||||
|
return nil, errs.ErrFailedToRequestRemoteApi
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.dataSource.ParseUserInfo(c, body)
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package oauth2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type nextcloudUserInfoResponse struct {
|
||||||
|
OCS *struct {
|
||||||
|
Meta *struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
StatusCode int `json:"statuscode"`
|
||||||
|
} `json:"meta"`
|
||||||
|
Data *struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
DisplayName string `json:"display-name"`
|
||||||
|
} `json:"data"`
|
||||||
|
} `json:"ocs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextcloudOAuth2DataSource represents Nextcloud OAuth 2.0 data source
|
||||||
|
type NextcloudOAuth2DataSource struct {
|
||||||
|
CommonOAuth2DataSource
|
||||||
|
baseUrl string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAuthUrl returns the authentication url of the Nextcloud data source
|
||||||
|
func (s *NextcloudOAuth2DataSource) GetAuthUrl() string {
|
||||||
|
// Reference: https://docs.nextcloud.com/server/stable/developer_manual/_static/openapi.html#/operations/oauth2-login_redirector-authorize
|
||||||
|
return s.baseUrl + "apps/oauth2/authorize"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTokenUrl returns the token url of the Nextcloud data source
|
||||||
|
func (s *NextcloudOAuth2DataSource) GetTokenUrl() string {
|
||||||
|
// Reference: https://docs.nextcloud.com/server/stable/developer_manual/_static/openapi.html#/operations/oauth2-oauth_api-get-token
|
||||||
|
return s.baseUrl + "apps/oauth2/api/v1/token"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfoRequest returns the user info request of the Nextcloud data source
|
||||||
|
func (s *NextcloudOAuth2DataSource) GetUserInfoRequest() (*http.Request, error) {
|
||||||
|
// Reference: https://docs.nextcloud.com/server/stable/developer_manual/_static/openapi.html#/operations/provisioning_api-users-get-current-user
|
||||||
|
req, err := http.NewRequest("GET", s.baseUrl+"ocs/v2.php/cloud/user", nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("OCS-APIRequest", "true")
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScopes returns the scopes required by the Nextcloud provider
|
||||||
|
func (p *NextcloudOAuth2DataSource) GetScopes() []string {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NextcloudOAuth2DataSource) ParseUserInfo(c core.Context, body []byte) (*OAuth2UserInfo, error) {
|
||||||
|
userInfoResp := &nextcloudUserInfoResponse{}
|
||||||
|
err := json.Unmarshal(body, &userInfoResp)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf(c, "[nextcloud_oauth2_datasource.ParseUserInfo] failed to parse user info response body, because %s", err.Error())
|
||||||
|
return nil, errs.ErrCannotRetrieveUserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if userInfoResp.OCS == nil || userInfoResp.OCS.Meta == nil || userInfoResp.OCS.Data == nil {
|
||||||
|
log.Warnf(c, "[nextcloud_oauth2_datasource.ParseUserInfo] invalid user info response body")
|
||||||
|
return nil, errs.ErrCannotRetrieveUserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if userInfoResp.OCS.Meta.StatusCode != 200 {
|
||||||
|
log.Warnf(c, "[nextcloud_oauth2_datasource.ParseUserInfo] user info response status code is %d", userInfoResp.OCS.Meta.StatusCode)
|
||||||
|
return nil, errs.ErrCannotRetrieveUserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if userInfoResp.OCS.Data.ID == "" {
|
||||||
|
log.Warnf(c, "[nextcloud_oauth2_datasource.ParseUserInfo] user info id is empty")
|
||||||
|
return nil, errs.ErrCannotRetrieveUserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
return &OAuth2UserInfo{
|
||||||
|
UserName: userInfoResp.OCS.Data.ID,
|
||||||
|
Email: userInfoResp.OCS.Data.Email,
|
||||||
|
NickName: userInfoResp.OCS.Data.DisplayName,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNextcloudOAuth2Provider creates a new Nextcloud OAuth 2.0 provider instance
|
||||||
|
func NewNextcloudOAuth2Provider(baseUrl string) OAuth2Provider {
|
||||||
|
if baseUrl[len(baseUrl)-1] != '/' {
|
||||||
|
baseUrl += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CommonOAuth2Provider{
|
||||||
|
dataSource: &NextcloudOAuth2DataSource{
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package oauth2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewNextcloudOAuth2Provider(t *testing.T) {
|
||||||
|
datasource := NewNextcloudOAuth2Provider("https://example.com/")
|
||||||
|
assert.Equal(t, "https://example.com/apps/oauth2/authorize", datasource.GetAuthUrl())
|
||||||
|
assert.Equal(t, "https://example.com/apps/oauth2/api/v1/token", datasource.GetTokenUrl())
|
||||||
|
|
||||||
|
datasource = NewNextcloudOAuth2Provider("https://example.com")
|
||||||
|
assert.Equal(t, "https://example.com/apps/oauth2/authorize", datasource.GetAuthUrl())
|
||||||
|
assert.Equal(t, "https://example.com/apps/oauth2/api/v1/token", datasource.GetTokenUrl())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextcloudOAuth2datasource_GetUserInfoRequest(t *testing.T) {
|
||||||
|
datasource := &NextcloudOAuth2DataSource{baseUrl: "https://example.com/"}
|
||||||
|
req, err := datasource.GetUserInfoRequest()
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "GET", req.Method)
|
||||||
|
assert.Equal(t, "https://example.com/ocs/v2.php/cloud/user", req.URL.String())
|
||||||
|
assert.Equal(t, "application/json", req.Header.Get("Accept"))
|
||||||
|
assert.Equal(t, "true", req.Header.Get("OCS-APIRequest"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextcloudOAuth2Datasource_ParseUserInfo_Success(t *testing.T) {
|
||||||
|
datasource := &NextcloudOAuth2DataSource{}
|
||||||
|
responseContent := `{
|
||||||
|
"ocs": {
|
||||||
|
"meta": {
|
||||||
|
"status": "ok",
|
||||||
|
"statuscode": 200
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"id": "user1",
|
||||||
|
"email": "user1@example.com",
|
||||||
|
"display-name": "User"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
info, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent))
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "user1", info.UserName)
|
||||||
|
assert.Equal(t, "user1@example.com", info.Email)
|
||||||
|
assert.Equal(t, "User", info.NickName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextcloudOAuth2Datasource_ParseUserInfo_InvalidJson(t *testing.T) {
|
||||||
|
datasource := &NextcloudOAuth2DataSource{}
|
||||||
|
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte("invalid"))
|
||||||
|
|
||||||
|
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextcloudOAuth2Datasource_ParseUserInfo_MissingFields(t *testing.T) {
|
||||||
|
datasource := &NextcloudOAuth2DataSource{}
|
||||||
|
responseContent := `{"ocs": {}}`
|
||||||
|
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent))
|
||||||
|
|
||||||
|
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextcloudOAuth2Datasource_ParseUserInfo_Non200StatusCode(t *testing.T) {
|
||||||
|
datasource := &NextcloudOAuth2DataSource{}
|
||||||
|
responseContent := `{
|
||||||
|
"ocs": {
|
||||||
|
"meta": {
|
||||||
|
"status": "error",
|
||||||
|
"statuscode": 400
|
||||||
|
},
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent))
|
||||||
|
|
||||||
|
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextcloudOAuth2Datasource_ParseUserInfo_EmptyID(t *testing.T) {
|
||||||
|
datasource := &NextcloudOAuth2DataSource{}
|
||||||
|
responseContent := `{
|
||||||
|
"ocs": {
|
||||||
|
"meta": {
|
||||||
|
"status": "ok",
|
||||||
|
"statuscode": 200
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"id": "",
|
||||||
|
"email": "user1@example.com",
|
||||||
|
"display-name": "User One"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
_, err := datasource.ParseUserInfo(core.NewNullContext(), []byte(responseContent))
|
||||||
|
|
||||||
|
assert.Equal(t, errs.ErrCannotRetrieveUserInfo, err)
|
||||||
|
}
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
package oauth2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type nextcloudUserInfoResponse struct {
|
|
||||||
OCS *struct {
|
|
||||||
Meta *struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
StatusCode int `json:"statuscode"`
|
|
||||||
} `json:"meta"`
|
|
||||||
Data *struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
DisplayName string `json:"display-name"`
|
|
||||||
} `json:"data"`
|
|
||||||
} `json:"ocs"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NextcloudOAuth2Provider represents Nextcloud OAuth 2.0 provider
|
|
||||||
type NextcloudOAuth2Provider struct {
|
|
||||||
baseUrl string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewNextcloudOAuth2Provider creates a new Nextcloud OAuth 2.0 provider instance
|
|
||||||
func NewNextcloudOAuth2Provider(baseUrl string) OAuth2Provider {
|
|
||||||
if baseUrl[len(baseUrl)-1] != '/' {
|
|
||||||
baseUrl += "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
return &NextcloudOAuth2Provider{
|
|
||||||
baseUrl: baseUrl,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAuthUrl returns the authentication url of the Nextcloud provider
|
|
||||||
func (p *NextcloudOAuth2Provider) GetAuthUrl() string {
|
|
||||||
return p.baseUrl + "apps/oauth2/authorize"
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTokenUrl returns the token url of the Nextcloud provider
|
|
||||||
func (p *NextcloudOAuth2Provider) GetTokenUrl() string {
|
|
||||||
return p.baseUrl + "apps/oauth2/api/v1/token"
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserInfo returns the user info by the Nextcloud provider
|
|
||||||
func (p *NextcloudOAuth2Provider) GetUserInfo(c core.Context, oauth2Client *http.Client) (*OAuth2UserInfo, error) {
|
|
||||||
url := p.baseUrl + "ocs/v2.php/cloud/user?format=json"
|
|
||||||
resp, err := oauth2Client.Get(url)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf(c, "[nextcloud_oauth2_provider.GetUserInfo] failed to get user info response, because %s", err.Error())
|
|
||||||
return nil, errs.ErrFailedToRequestRemoteApi
|
|
||||||
}
|
|
||||||
|
|
||||||
defer resp.Body.Close()
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
|
|
||||||
log.Debugf(c, "[nextcloud_oauth2_provider.GetUserInfo] response is %s", body)
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
log.Errorf(c, "[nextcloud_oauth2_provider.GetUserInfo] failed to get user info response, because response code is %d", resp.StatusCode)
|
|
||||||
return nil, errs.ErrFailedToRequestRemoteApi
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.parseUserInfo(c, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetScopes returns the scopes required by the Nextcloud provider
|
|
||||||
func (p *NextcloudOAuth2Provider) GetScopes() []string {
|
|
||||||
return []string{"profile", "email"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *NextcloudOAuth2Provider) parseUserInfo(c core.Context, body []byte) (*OAuth2UserInfo, error) {
|
|
||||||
userInfoResp := &nextcloudUserInfoResponse{}
|
|
||||||
err := json.Unmarshal(body, &userInfoResp)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf(c, "[nextcloud_oauth2_provider.parseUserInfo] failed to parse user info response body, because %s", err.Error())
|
|
||||||
return nil, errs.ErrCannotRetrieveUserInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
if userInfoResp.OCS == nil || userInfoResp.OCS.Meta == nil || userInfoResp.OCS.Data == nil {
|
|
||||||
log.Warnf(c, "[nextcloud_oauth2_provider.parseUserInfo] invalid user info response body")
|
|
||||||
return nil, errs.ErrCannotRetrieveUserInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
if userInfoResp.OCS.Meta.StatusCode != 200 {
|
|
||||||
log.Warnf(c, "[nextcloud_oauth2_provider.parseUserInfo] user info response status code is %d", userInfoResp.OCS.Meta.StatusCode)
|
|
||||||
return nil, errs.ErrCannotRetrieveUserInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
if userInfoResp.OCS.Data.ID == "" {
|
|
||||||
log.Warnf(c, "[nextcloud_oauth2_provider.parseUserInfo] user info id is empty")
|
|
||||||
return nil, errs.ErrCannotRetrieveUserInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
return &OAuth2UserInfo{
|
|
||||||
UserName: userInfoResp.OCS.Data.ID,
|
|
||||||
Email: userInfoResp.OCS.Data.Email,
|
|
||||||
NickName: userInfoResp.OCS.Data.DisplayName,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user