1// Package nextcloud implements the OAuth2 protocol for authenticating users through nextcloud. 2// This package can be used as a reference implementation of an OAuth2 provider for Goth. 3package nextcloud 4 5import ( 6 "bytes" 7 "encoding/json" 8 "io" 9 "io/ioutil" 10 "net/http" 11 12 "fmt" 13 14 "github.com/markbates/goth" 15 "golang.org/x/oauth2" 16) 17 18// These vars define the Authentication, Token, and Profile URLS for Nextcloud. 19// You have to set these values to something useful, because nextcloud is always 20// hosted somewhere. 21// 22var ( 23 AuthURL = "https://<own-server>/apps/oauth2/authorize" 24 TokenURL = "https://<own-server>/apps/oauth2/api/v1/token" 25 ProfileURL = "https://<own-server>/ocs/v2.php/cloud/user?format=json" 26) 27 28// Provider is the implementation of `goth.Provider` for accessing Nextcloud. 29type Provider struct { 30 ClientKey string 31 Secret string 32 CallbackURL string 33 HTTPClient *http.Client 34 config *oauth2.Config 35 providerName string 36 authURL string 37 tokenURL string 38 profileURL string 39} 40 41// New is only here to fulfill the interface requirements and does not work properly without 42// setting your own Nextcloud connect parameters, more precisely AuthURL, TokenURL and ProfileURL. 43// Please use NewCustomisedDNS with the beginning of your URL or NewCustomiseURL. 44func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { 45 return NewCustomisedURL(clientKey, secret, callbackURL, AuthURL, TokenURL, ProfileURL, scopes...) 46} 47 48// NewCustomisedURL create a working connection to your Nextcloud server given by the values 49// authURL, tokenURL and profileURL. 50// If you want to use a simpler method, please have a look at NewCustomisedDNS, which gets only 51// on parameter instead of three. 52func NewCustomisedURL(clientKey, secret, callbackURL, authURL, tokenURL, profileURL string, scopes ...string) *Provider { 53 p := &Provider{ 54 ClientKey: clientKey, 55 Secret: secret, 56 CallbackURL: callbackURL, 57 providerName: "nextcloud", 58 profileURL: profileURL, 59 } 60 p.config = newConfig(p, authURL, tokenURL, scopes) 61 return p 62} 63 64// NewCustomisedDNS is the simplest method to create a provider based only on your key/secret 65// and the beginning of the URL to your server, e.g. https://my.server.name/ 66func NewCustomisedDNS(clientKey, secret, callbackURL, nextcloudURL string, scopes ...string) *Provider { 67 return NewCustomisedURL( 68 clientKey, 69 secret, 70 callbackURL, 71 nextcloudURL+"/apps/oauth2/authorize", 72 nextcloudURL+"/apps/oauth2/api/v1/token", 73 nextcloudURL+"/ocs/v2.php/cloud/user?format=json", 74 scopes..., 75 ) 76} 77 78// Name is the name used to retrieve this provider later. 79func (p *Provider) Name() string { 80 return p.providerName 81} 82 83// SetName is to update the name of the provider (needed in case of multiple providers of 1 type) 84func (p *Provider) SetName(name string) { 85 p.providerName = name 86} 87 88func (p *Provider) Client() *http.Client { 89 return goth.HTTPClientWithFallBack(p.HTTPClient) 90} 91 92// Debug is a no-op for the nextcloud package. 93func (p *Provider) Debug(debug bool) {} 94 95// BeginAuth asks Nextcloud for an authentication end-point. 96func (p *Provider) BeginAuth(state string) (goth.Session, error) { 97 return &Session{ 98 AuthURL: p.config.AuthCodeURL(state), 99 }, nil 100} 101 102// FetchUser will go to Nextcloud and access basic information about the user. 103func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { 104 sess := session.(*Session) 105 user := goth.User{ 106 AccessToken: sess.AccessToken, 107 Provider: p.Name(), 108 RefreshToken: sess.RefreshToken, 109 ExpiresAt: sess.ExpiresAt, 110 } 111 112 if user.AccessToken == "" { 113 // data is not yet retrieved since accessToken is still empty 114 return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) 115 } 116 117 req, err := http.NewRequest("GET", p.profileURL, nil) 118 if err != nil { 119 return user, err 120 } 121 req.Header.Set("Authorization", "Bearer "+sess.AccessToken) 122 response, err := p.Client().Do(req) 123 124 if err != nil { 125 if response != nil { 126 response.Body.Close() 127 } 128 return user, err 129 } 130 131 defer response.Body.Close() 132 133 if response.StatusCode != http.StatusOK { 134 return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode) 135 } 136 137 bits, err := ioutil.ReadAll(response.Body) 138 if err != nil { 139 return user, err 140 } 141 142 err = json.NewDecoder(bytes.NewReader(bits)).Decode(&user.RawData) 143 if err != nil { 144 return user, err 145 } 146 147 err = userFromReader(bytes.NewReader(bits), &user) 148 149 return user, err 150} 151 152func newConfig(provider *Provider, authURL, tokenURL string, scopes []string) *oauth2.Config { 153 c := &oauth2.Config{ 154 ClientID: provider.ClientKey, 155 ClientSecret: provider.Secret, 156 RedirectURL: provider.CallbackURL, 157 Endpoint: oauth2.Endpoint{ 158 AuthURL: authURL, 159 TokenURL: tokenURL, 160 }, 161 Scopes: []string{}, 162 } 163 164 if len(scopes) > 0 { 165 for _, scope := range scopes { 166 c.Scopes = append(c.Scopes, scope) 167 } 168 } 169 return c 170} 171 172func userFromReader(r io.Reader, user *goth.User) error { 173 u := struct { 174 Ocs struct { 175 Data struct { 176 EMail string `json:"email"` 177 DisplayName string `json:"display-name"` 178 ID string `json:"id"` 179 Address string `json:"address"` 180 } 181 } `json:"ocs"` 182 }{} 183 err := json.NewDecoder(r).Decode(&u) 184 if err != nil { 185 return err 186 } 187 user.Email = u.Ocs.Data.EMail 188 user.Name = u.Ocs.Data.DisplayName 189 user.UserID = u.Ocs.Data.ID 190 user.Location = u.Ocs.Data.Address 191 return nil 192} 193 194//RefreshTokenAvailable refresh token is provided by auth provider or not 195func (p *Provider) RefreshTokenAvailable() bool { 196 return true 197} 198 199//RefreshToken get new access token based on the refresh token 200func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { 201 token := &oauth2.Token{RefreshToken: refreshToken} 202 ts := p.config.TokenSource(goth.ContextForClient(p.Client()), token) 203 newToken, err := ts.Token() 204 if err != nil { 205 return nil, err 206 } 207 return newToken, err 208} 209