Skip to content

Commit 82d8b30

Browse files
committed
Initial commit
1 parent fa31139 commit 82d8b30

File tree

5 files changed

+205
-2
lines changed

5 files changed

+205
-2
lines changed

LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@
186186
same "printed page" as the copyright notice for easier
187187
identification within third-party archives.
188188

189-
Copyright [yyyy] [name of copyright owner]
189+
Copyright 2018 Hidetake Iwata
190190

191191
Licensed under the Apache License, Version 2.0 (the "License");
192192
you may not use this file except in compliance with the License.

README.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,12 @@
1-
# oauth2cli
1+
# Package oauth2cli
2+
3+
A Golang library for using OAuth 2.0 or OpenID Connect on a command line tool.
4+
5+
6+
## Why
7+
8+
TODO
9+
10+
## How it works
11+
12+
TODO

authcode.go

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package oauth2cli
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"net"
8+
"net/http"
9+
"strconv"
10+
"strings"
11+
"time"
12+
13+
"github.com/pkg/browser"
14+
"golang.org/x/oauth2"
15+
)
16+
17+
// AuthCodeFlow provides flow with OAuth 2.0 Authorization Code Grant.
18+
// See https://tools.ietf.org/html/rfc6749#section-4.1
19+
type AuthCodeFlow struct {
20+
// OAuth2 configuration.
21+
// RedirectURL will be set to "http://localhost:port" if it is empty.
22+
Config oauth2.Config
23+
24+
AuthCodeOptions []oauth2.AuthCodeOption // Options passed to AuthCodeURL().
25+
ServerPort int // HTTP server port. Default to a random port.
26+
SkipOpenBrowser bool // Skip opening browser if it is true.
27+
}
28+
29+
// GetToken retrieves a token from the provider.
30+
func (f *AuthCodeFlow) GetToken(ctx context.Context) (*oauth2.Token, error) {
31+
code, err := f.getAuthCode(ctx)
32+
if err != nil {
33+
return nil, fmt.Errorf("Could not get an auth code: %s", err)
34+
}
35+
token, err := f.Config.Exchange(ctx, code)
36+
if err != nil {
37+
return nil, fmt.Errorf("Could not exchange token: %s", err)
38+
}
39+
return token, nil
40+
}
41+
42+
func (f *AuthCodeFlow) getAuthCode(ctx context.Context) (string, error) {
43+
state, err := newOAuth2State()
44+
if err != nil {
45+
return "", fmt.Errorf("Could not generate state parameter: %s", err)
46+
}
47+
codeCh := make(chan string)
48+
defer close(codeCh)
49+
errCh := make(chan error)
50+
defer close(errCh)
51+
52+
listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", f.ServerPort))
53+
if err != nil {
54+
return "", fmt.Errorf("Could not listen to port %d", f.ServerPort)
55+
}
56+
defer listener.Close()
57+
port, err := extractPort(listener.Addr())
58+
if err != nil {
59+
return "", fmt.Errorf("Could not determine listening port: %s", err)
60+
}
61+
log.Printf("Listening to port %d", port)
62+
if f.Config.RedirectURL == "" {
63+
f.Config.RedirectURL = fmt.Sprintf("http://localhost:%d/", port)
64+
}
65+
66+
server := &http.Server{
67+
Handler: &authCodeHandler{
68+
authCodeURL: f.Config.AuthCodeURL(string(state), f.AuthCodeOptions...),
69+
gotCode: func(code string, gotState oauth2State) {
70+
if gotState == state {
71+
codeCh <- code
72+
} else {
73+
errCh <- fmt.Errorf("State does not match, wants %s but %s", state, gotState)
74+
}
75+
},
76+
gotError: func(err error) {
77+
errCh <- err
78+
},
79+
},
80+
}
81+
defer server.Shutdown(ctx)
82+
go func() {
83+
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
84+
errCh <- err
85+
}
86+
}()
87+
go func() {
88+
log.Printf("Open http://localhost:%d for authorization", port)
89+
if !f.SkipOpenBrowser {
90+
time.Sleep(500 * time.Millisecond)
91+
browser.OpenURL(fmt.Sprintf("http://localhost:%d/", port))
92+
}
93+
}()
94+
select {
95+
case err := <-errCh:
96+
return "", err
97+
case code := <-codeCh:
98+
return code, nil
99+
case <-ctx.Done():
100+
return "", ctx.Err()
101+
}
102+
}
103+
104+
func extractPort(addr net.Addr) (int, error) {
105+
s := strings.SplitN(addr.String(), ":", 2)
106+
if len(s) != 2 {
107+
return 0, fmt.Errorf("Invalid address: %s", addr)
108+
}
109+
p, err := strconv.Atoi(s[1])
110+
if err != nil {
111+
return 0, fmt.Errorf("Not number %s: %s", addr, err)
112+
}
113+
return p, nil
114+
}
115+
116+
type authCodeHandler struct {
117+
authCodeURL string
118+
gotCode func(code string, state oauth2State)
119+
gotError func(err error)
120+
}
121+
122+
func (h *authCodeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
123+
log.Printf("%s %s", r.Method, r.RequestURI)
124+
m := r.Method
125+
p := r.URL.Path
126+
q := r.URL.Query()
127+
switch {
128+
case m == "GET" && p == "/" && q.Get("error") != "":
129+
h.gotError(fmt.Errorf("OAuth Error: %s %s", q.Get("error"), q.Get("error_description")))
130+
http.Error(w, "OAuth Error", 500)
131+
132+
case m == "GET" && p == "/" && q.Get("code") != "":
133+
h.gotCode(q.Get("code"), oauth2State(q.Get("state")))
134+
w.Header().Add("Content-Type", "text/html")
135+
fmt.Fprintf(w, `<html><body>OK<script>window.close()</script></body></html>`)
136+
137+
case m == "GET" && p == "/":
138+
http.Redirect(w, r, h.authCodeURL, 302)
139+
140+
default:
141+
http.Error(w, "Not Found", 404)
142+
}
143+
}

authcode_test.go

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package oauth2cli_test
2+
3+
import (
4+
"context"
5+
"log"
6+
7+
"github.com/int128/oauth2cli"
8+
"golang.org/x/oauth2"
9+
)
10+
11+
var endpoint = oauth2.Endpoint{
12+
AuthURL: "https://example.com/oauth2/auth",
13+
TokenURL: "https://example.com/oauth2/token",
14+
}
15+
16+
func ExampleAuthCodeFlow() {
17+
ctx := context.Background()
18+
flow := oauth2cli.AuthCodeFlow{
19+
Config: oauth2.Config{
20+
ClientID: "YOUR_CLIENT_ID",
21+
ClientSecret: "YOUR_CLIENT_SECRET",
22+
Endpoint: endpoint,
23+
Scopes: []string{"email"},
24+
},
25+
}
26+
token, err := flow.GetToken(ctx)
27+
if err != nil {
28+
log.Fatalf("Could not get a token: %s", err)
29+
}
30+
log.Printf("Got access token %s", token.AccessToken)
31+
}

oauth2cli.go

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Package oauth2cli provides ...
2+
package oauth2cli
3+
4+
import (
5+
"crypto/rand"
6+
"encoding/binary"
7+
"fmt"
8+
)
9+
10+
type oauth2State string
11+
12+
func newOAuth2State() (oauth2State, error) {
13+
var n uint64
14+
if err := binary.Read(rand.Reader, binary.LittleEndian, &n); err != nil {
15+
return "", err
16+
}
17+
return oauth2State(fmt.Sprintf("%x", n)), nil
18+
}

0 commit comments

Comments
 (0)