-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgithubauthorizationhandler.go
More file actions
119 lines (102 loc) · 3.24 KB
/
Copy pathgithubauthorizationhandler.go
File metadata and controls
119 lines (102 loc) · 3.24 KB
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
package cocopilot
import (
"context"
"fmt"
"net"
"net/http"
"os"
"time"
"github.com/pkg/browser"
"golang.org/x/oauth2/authhandler"
)
const (
httpTimeout time.Duration = 30 * time.Second
)
var _ authhandler.AuthorizationHandler = (*GithubDeviceAuthGrantFlowHandler)(
nil,
).AuthorizationHandler
type (
// Code The authorization code generated by the authorization server.
// See https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2
Code = string
// State opaque value used by the client to maintain state between the request
// and callback.
// See https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1
State = string
)
// GithubDeviceAuthGrantFlowHandler implements an OAuth 2.0 Device Authorization
// Grant flow handler with GitHub-flavored specifics. It creates a listener and
// HTTP server to handle the callback.
//
//nolint:containedctx // unfortunately [authhandler.AuthorizationHandler] doesn't take a context
type GithubDeviceAuthGrantFlowHandler struct {
context.Context
result chan *AuthorizationResponse
}
// AuthorizationHandler initiates an authorization flow by running a HTTP server
// and waiting for a callback request.
func (handler *GithubDeviceAuthGrantFlowHandler) AuthorizationHandler(
authCodeURL string,
) (Code, State, error) {
listener, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
return "", "", fmt.Errorf("failed to create network listener: %w", err)
}
defer listener.Close()
handler.result = make(chan *AuthorizationResponse, 1)
server := http.Server{
ReadTimeout: httpTimeout,
ReadHeaderTimeout: httpTimeout,
BaseContext: func(_ net.Listener) context.Context { return handler.Context },
Handler: http.HandlerFunc(handler.callbackHandler),
}
//nolint:errcheck // we don't care about server errors
go server.Serve(listener)
defer server.Close()
err = browser.OpenURL(authCodeURL)
if err != nil {
return "", "", fmt.Errorf("failed to open browser: %w", err)
}
select {
case <-handler.Done():
return "", "", fmt.Errorf(
"authorization handling halted: %w",
context.Cause(handler.Context),
)
case res := <-handler.result:
if err := res.Error(); err != nil {
return "", "", fmt.Errorf("failed to authorize: %w", res.Error())
}
return res.Code, res.State, nil
}
}
func (handler *GithubDeviceAuthGrantFlowHandler) callbackHandler(
w http.ResponseWriter,
req *http.Request,
) {
res := &AuthorizationResponse{
Code: req.URL.Query().Get("code"),
State: req.URL.Query().Get("state"),
ErrorCode: req.URL.Query().Get("error"),
ErrorDescription: req.URL.Query().Get("error_description"),
ErrorURI: req.URL.Query().Get("error_uri"),
}
// we play nice and give some status report to the user in their browser
if err := res.Error(); err != nil {
http.Error(w, res.Error().Error(), http.StatusInternalServerError)
} else {
_, err = w.Write([]byte("Authorized successfully. You can close this tab/window now."))
if err != nil {
fmt.Fprintf(os.Stderr, "failed to write callback response: %s", err)
}
}
// flush if supported, otherwise the shutdown prevents a friendly response
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
select {
case <-handler.Done():
return
case handler.result <- res:
}
}