1/*
2Copyright 2017-2018 Mikael Berthe
3
4Licensed under the MIT license.  Please see the LICENSE file is this directory.
5*/
6
7package madon
8
9import (
10	"encoding/json"
11	"strings"
12
13	"golang.org/x/net/context"
14	"golang.org/x/oauth2"
15
16	"github.com/pkg/errors"
17	"github.com/sendgrid/rest"
18)
19
20const oAuthRelPath = "/oauth/"
21
22// UserToken represents a user token as returned by the Mastodon API
23type UserToken struct {
24	AccessToken string `json:"access_token"`
25	CreatedAt   int64  `json:"created_at"`
26	Scope       string `json:"scope"`
27	TokenType   string `json:"token_type"`
28}
29
30// LoginBasic does basic user authentication
31func (mc *Client) LoginBasic(username, password string, scopes []string) error {
32	if mc == nil {
33		return ErrUninitializedClient
34	}
35
36	if username == "" {
37		return errors.New("missing username")
38	}
39	if password == "" {
40		return errors.New("missing password")
41	}
42
43	hdrs := make(map[string]string)
44	opts := make(map[string]string)
45
46	hdrs["User-Agent"] = "madon/" + MadonVersion
47
48	opts["grant_type"] = "password"
49	opts["client_id"] = mc.ID
50	opts["client_secret"] = mc.Secret
51	opts["username"] = username
52	opts["password"] = password
53	if len(scopes) > 0 {
54		opts["scope"] = strings.Join(scopes, " ")
55	}
56
57	req := rest.Request{
58		BaseURL:     mc.InstanceURL + oAuthRelPath + "token",
59		Headers:     hdrs,
60		QueryParams: opts,
61		Method:      rest.Post,
62	}
63
64	r, err := restAPI(req)
65	if err != nil {
66		return err
67	}
68
69	var resp UserToken
70
71	err = json.Unmarshal([]byte(r.Body), &resp)
72	if err != nil {
73		return errors.Wrap(err, "cannot unmarshal server response")
74	}
75
76	mc.UserToken = &resp
77	return nil
78}
79
80// SetUserToken sets an existing user credentials
81// No verification of the arguments is made.
82func (mc *Client) SetUserToken(token, username, password string, scopes []string) error {
83	if mc == nil {
84		return ErrUninitializedClient
85	}
86
87	mc.UserToken = &UserToken{
88		AccessToken: token,
89		Scope:       strings.Join(scopes, " "),
90		TokenType:   "bearer",
91	}
92	return nil
93}
94
95// LoginOAuth2 handles OAuth2 authentication
96// If code is empty, the URL to the server consent page will be returned;
97// if not, the user token is set.
98func (mc *Client) LoginOAuth2(code string, scopes []string) (string, error) {
99	if mc == nil {
100		return "", ErrUninitializedClient
101	}
102
103	conf := &oauth2.Config{
104		ClientID:     mc.ID,
105		ClientSecret: mc.Secret,
106		Scopes:       scopes,
107		Endpoint: oauth2.Endpoint{
108			AuthURL:  mc.InstanceURL + oAuthRelPath + "authorize",
109			TokenURL: mc.InstanceURL + oAuthRelPath + "token",
110		},
111		RedirectURL: NoRedirect,
112	}
113
114	if code == "" {
115		// URL to consent page to ask for permission
116		// for the scopes specified above.
117		return conf.AuthCodeURL("state", oauth2.AccessTypeOffline), nil
118	}
119
120	// Return token
121	t, err := conf.Exchange(context.TODO(), code)
122	if err != nil {
123		return "", errors.Wrap(err, "cannot convert code into a token")
124	}
125	if t == nil || t.AccessToken == "" {
126		return "", errors.New("empty token")
127	}
128	return "", mc.SetUserToken(t.AccessToken, "", "", scopes)
129}
130