/
live.go
187 lines (173 loc) · 6.98 KB
/
live.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
package auth
import (
"encoding/json"
"fmt"
"golang.org/x/oauth2"
"golang.org/x/oauth2/microsoft"
"io"
"net/http"
"net/url"
"os"
"time"
)
// TokenSource holds an oauth2.TokenSource which uses device auth to get a code. The user authenticates using
// a code. TokenSource prints the authentication code and URL to os.Stdout. To use a different io.Writer, use
// WriterTokenSource. TokenSource automatically refreshes tokens.
var TokenSource oauth2.TokenSource = &tokenSource{w: os.Stdout}
// WriterTokenSource returns a new oauth2.TokenSource which, like TokenSource, uses device auth to get a code.
// Unlike TokenSource, WriterTokenSource allows passing an io.Writer to which information on the auth URL and
// code are printed. WriterTokenSource automatically refreshes tokens.
func WriterTokenSource(w io.Writer) oauth2.TokenSource {
return &tokenSource{w: w}
}
// tokenSource implements the oauth2.TokenSource interface. It provides a method to get an oauth2.Token using
// device auth through a call to RequestLiveToken.
type tokenSource struct {
w io.Writer
t *oauth2.Token
}
// Token attempts to return a Live Connect token using the RequestLiveToken function.
func (src *tokenSource) Token() (*oauth2.Token, error) {
if src.t == nil {
t, err := RequestLiveTokenWriter(src.w)
src.t = t
return t, err
}
tok, err := refreshToken(src.t)
if err != nil {
return nil, err
}
// Update the token to use to refresh for the next time Token is called.
src.t = tok
return tok, nil
}
// RefreshTokenSource returns a new oauth2.TokenSource using the oauth2.Token passed that automatically
// refreshes the token everytime it expires. Note that this function must be used over oauth2.ReuseTokenSource
// due to that function not refreshing with the correct scopes.
func RefreshTokenSource(t *oauth2.Token) oauth2.TokenSource {
return oauth2.ReuseTokenSource(t, &tokenSource{w: os.Stdout, t: t})
}
// RequestLiveToken does a login request for Microsoft Live Connect using device auth. A login URL will be
// printed to the stdout with a user code which the user must use to submit.
// RequestLiveToken is the equivalent of RequestLiveTokenWriter(os.Stdout).
func RequestLiveToken() (*oauth2.Token, error) {
return RequestLiveTokenWriter(os.Stdout)
}
// RequestLiveTokenWriter does a login request for Microsoft Live Connect using device auth. A login URL will
// be printed to the io.Writer passed with a user code which the user must use to submit.
// Once fully authenticated, an oauth2 token is returned which may be used to login to XBOX Live.
func RequestLiveTokenWriter(w io.Writer) (*oauth2.Token, error) {
d, err := startDeviceAuth()
if err != nil {
return nil, err
}
_, _ = w.Write([]byte(fmt.Sprintf("Authenticate at %v using the code %v.\n", d.VerificationURI, d.UserCode)))
ticker := time.NewTicker(time.Second * time.Duration(d.Interval))
defer ticker.Stop()
for range ticker.C {
t, err := pollDeviceAuth(d.DeviceCode)
if err != nil {
return nil, fmt.Errorf("error polling for device auth: %w", err)
}
// If the token could not be obtained yet (authentication wasn't finished yet), the token is nil.
// We just retry if this is the case.
if t != nil {
_, _ = w.Write([]byte("Authentication successful.\n"))
return t, nil
}
}
panic("unreachable")
}
// startDeviceAuth starts the device auth, retrieving a login URI for the user and a code the user needs to
// enter.
func startDeviceAuth() (*deviceAuthConnect, error) {
resp, err := http.PostForm("https://login.live.com/oauth20_connect.srf", url.Values{
"client_id": {"0000000048183522"},
"scope": {"service::user.auth.xboxlive.com::MBI_SSL"},
"response_type": {"device_code"},
})
if err != nil {
return nil, fmt.Errorf("POST https://login.live.com/oauth20_connect.srf: %w", err)
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("POST https://login.live.com/oauth20_connect.srf: %v", resp.Status)
}
data := new(deviceAuthConnect)
return data, json.NewDecoder(resp.Body).Decode(data)
}
// pollDeviceAuth polls the token endpoint for the device code. A token is returned if the user authenticated
// successfully. If the user has not yet authenticated, err is nil but the token is nil too.
func pollDeviceAuth(deviceCode string) (t *oauth2.Token, err error) {
resp, err := http.PostForm(microsoft.LiveConnectEndpoint.TokenURL, url.Values{
"client_id": {"0000000048183522"},
"grant_type": {"urn:ietf:params:oauth:grant-type:device_code"},
"device_code": {deviceCode},
})
if err != nil {
return nil, fmt.Errorf("POST https://login.live.com/oauth20_token.srf: %w", err)
}
poll := new(deviceAuthPoll)
if err := json.NewDecoder(resp.Body).Decode(poll); err != nil {
return nil, fmt.Errorf("POST https://login.live.com/oauth20_token.srf: json decode: %w", err)
}
_ = resp.Body.Close()
if poll.Error == "authorization_pending" {
return nil, nil
} else if poll.Error == "" {
return &oauth2.Token{
AccessToken: poll.AccessToken,
TokenType: poll.TokenType,
RefreshToken: poll.RefreshToken,
Expiry: time.Now().Add(time.Duration(poll.ExpiresIn) * time.Second),
}, nil
}
return nil, fmt.Errorf("non-empty unknown poll error: %v", poll.Error)
}
// refreshToken refreshes the oauth2.Token passed and returns a new oauth2.Token. An error is returned if
// refreshing was not successful.
func refreshToken(t *oauth2.Token) (*oauth2.Token, error) {
// This function unfortunately needs to exist because golang.org/x/oauth2 does not pass the scope to this
// request, which Microsoft Connect enforces.
resp, err := http.PostForm(microsoft.LiveConnectEndpoint.TokenURL, url.Values{
"client_id": {"0000000048183522"},
"scope": {"service::user.auth.xboxlive.com::MBI_SSL"},
"grant_type": {"refresh_token"},
"refresh_token": {t.RefreshToken},
})
if err != nil {
return nil, fmt.Errorf("POST https://login.live.com/oauth20_token.srf: %w", err)
}
poll := new(deviceAuthPoll)
if err := json.NewDecoder(resp.Body).Decode(poll); err != nil {
return nil, fmt.Errorf("POST https://login.live.com/oauth20_token.srf: json decode: %w", err)
}
_ = resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("POST https://login.live.com/oauth20_token.srf: refresh error: %v", poll.Error)
}
return &oauth2.Token{
AccessToken: poll.AccessToken,
TokenType: poll.TokenType,
RefreshToken: poll.RefreshToken,
Expiry: time.Now().Add(time.Duration(poll.ExpiresIn) * time.Second),
}, nil
}
type deviceAuthConnect struct {
UserCode string `json:"user_code"`
DeviceCode string `json:"device_code"`
VerificationURI string `json:"verification_uri"`
Interval int `json:"interval"`
ExpiresIn int `json:"expiresIn"`
}
type deviceAuthPoll struct {
Error string `json:"error"`
UserID string `json:"user_id"`
TokenType string `json:"token_type"`
Scope string `json:"scope"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
}