aboutsummaryrefslogtreecommitdiff
path: root/auth
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2021-11-16 14:46:24 -0800
committerMike Crute <mike@crute.us>2021-11-17 07:56:10 -0800
commitcc58a3da7d647de8520e33dc4356672d2ed1a366 (patch)
tree1b232a0d51446eb6370cfb13932190d31ce053df /auth
parenta42d794a286154a3106551e6e483861af2a9ef16 (diff)
downloadcloud-identity-broker-cc58a3da7d647de8520e33dc4356672d2ed1a366.tar.bz2
cloud-identity-broker-cc58a3da7d647de8520e33dc4356672d2ed1a366.tar.xz
cloud-identity-broker-cc58a3da7d647de8520e33dc4356672d2ed1a366.zip
Import of source code
Diffstat (limited to 'auth')
-rw-r--r--auth/github/github.go127
-rw-r--r--auth/jwt.go123
2 files changed, 250 insertions, 0 deletions
diff --git a/auth/github/github.go b/auth/github/github.go
new file mode 100644
index 0000000..b403296
--- /dev/null
+++ b/auth/github/github.go
@@ -0,0 +1,127 @@
1package github
2
3import (
4 "crypto/rand"
5 "encoding/base64"
6 "encoding/json"
7 "fmt"
8 "io/ioutil"
9 "net/http"
10 "net/url"
11 "strings"
12)
13
14type apiLoginResponse struct {
15 Username string `json:"login"`
16}
17
18type GitHubToken struct {
19 AccessToken string `json:"access_token"`
20 RefreshToken string `json:"refresh_token"`
21}
22
23type GitHubAuthenticator struct {
24 ClientId string
25 ClientSecret string
26}
27
28// GetAuthRedirect returns a redirect URL and a state parameter that should be
29// stored in the user's browser to correlate the authentication result and
30// request.
31func (a *GitHubAuthenticator) GetAuthRedirect() (string, string) {
32 sr := make([]byte, 32)
33 if _, err := rand.Read(sr); err != nil {
34 panic(err) // This should only happen if no entropy available
35 }
36 random := base64.URLEncoding.EncodeToString(sr)
37
38 return (&url.URL{
39 Scheme: "https",
40 Host: "github.com",
41 Path: "/login/oauth/authorize",
42 RawQuery: url.Values{
43 "client_id": []string{a.ClientId},
44 "scope": []string{"read:user"},
45 "state": []string{random},
46 "allow_signup": []string{"false"},
47 }.Encode(),
48 }).String(), random
49}
50
51// GetTokens returns the GitHub Oauth tokens from the service given a token
52// request code. The returned tokens are used for authentication to the GitHub
53// API.
54//
55// This relies on a GitHub Oauth application on the GitHub side to act as the
56// client for these tokens.
57func (a *GitHubAuthenticator) GetTokens(code string) (*GitHubToken, error) {
58 r, err := (&http.Client{}).Do(&http.Request{
59 Method: http.MethodPost,
60 URL: &url.URL{
61 Scheme: "https",
62 Host: "github.com",
63 Path: "/login/oauth/access_token",
64 },
65 Header: http.Header{
66 "Content-Type": []string{"application/x-www-form-urlencoded"},
67 "Accept": []string{"application/json"},
68 },
69 Body: ioutil.NopCloser(strings.NewReader(url.Values{
70 "client_id": []string{a.ClientId},
71 "client_secret": []string{a.ClientSecret},
72 "code": []string{code},
73 }.Encode())),
74 })
75 if err != nil {
76 return nil, err
77 }
78 defer r.Body.Close()
79
80 body, err := ioutil.ReadAll(r.Body)
81 if err != nil {
82 return nil, err
83 }
84
85 var ret GitHubToken
86 if err = json.Unmarshal(body, &ret); err != nil {
87 return nil, err
88 }
89
90 return &ret, nil
91}
92
93// GetUsernameWithToken returns the authenticated user's GitHub username given
94// an Oauth auth token.
95func (a *GitHubAuthenticator) GetUsernameWithToken(accessToken string) (string, error) {
96 r, err := (&http.Client{}).Do(&http.Request{
97 Method: http.MethodGet,
98 URL: &url.URL{
99 Scheme: "https",
100 Host: "api.github.com",
101 Path: "/user",
102 },
103 Header: http.Header{
104 "Authorization": []string{fmt.Sprintf("token %s", accessToken)},
105 },
106 })
107 if err != nil {
108 return "", err
109 }
110 defer r.Body.Close()
111
112 body, err := ioutil.ReadAll(r.Body)
113 if err != nil {
114 return "", err
115 }
116
117 var data apiLoginResponse
118 if err = json.Unmarshal(body, &data); err != nil {
119 return "", err
120 }
121
122 if data.Username == "" {
123 return "", fmt.Errorf("No user returned in GitHub response")
124 }
125
126 return data.Username, nil
127}
diff --git a/auth/jwt.go b/auth/jwt.go
new file mode 100644
index 0000000..c65cf39
--- /dev/null
+++ b/auth/jwt.go
@@ -0,0 +1,123 @@
1package auth
2
3import (
4 "context"
5 "fmt"
6 "time"
7
8 "code.crute.us/mcrute/cloud-identity-broker/app/models"
9
10 "gopkg.in/square/go-jose.v2"
11 "gopkg.in/square/go-jose.v2/jwt"
12)
13
14const keyIdHeader = jose.HeaderKey("kid")
15
16type JWTManager struct {
17 Store models.UserStore
18 Audience string
19 TokenExpires time.Duration
20}
21
22// Validate performs validate of a JWT for authentication. If the
23// authentication was successful it will return the user model for the
24// authenticated user.
25//
26// JWTs are signed by a per-user key on the service side. These keys may be
27// generated automatically when issuing a token to the user or, in the case of
28// service accounts, they may be generated by the user and validated on the
29// service side with the user's public key.
30func (m *JWTManager) Validate(j string) (*models.User, error) {
31 ctx := context.Background()
32
33 parsed, err := jwt.ParseSigned(j)
34 if err != nil {
35 return nil, err
36 }
37
38 // With a single signature there should be exactly one header. Without that
39 // we can't get the key id which means we won't be able to get the key and
40 // validate the signature.
41 if len(parsed.Headers) != 1 {
42 return nil, fmt.Errorf("Expected exactly 1 JWT header, got %d", len(parsed.Headers))
43 }
44
45 kid := parsed.Headers[0].KeyID
46 if kid == "" {
47 return nil, fmt.Errorf("No key ID in token header")
48 }
49
50 // We need the subject claim to lookup the user so we can get their key and
51 // validate the token signature.
52 untrustedClaims := jwt.Claims{}
53 if err := parsed.UnsafeClaimsWithoutVerification(&untrustedClaims); err != nil {
54 return nil, err
55 }
56
57 user, err := m.Store.Get(ctx, untrustedClaims.Subject)
58 if err != nil {
59 return nil, err
60 }
61
62 key := user.GetKey(kid) // Will not return invalid keys
63 if key == nil {
64 return nil, fmt.Errorf("No key in user record for key id %s", kid)
65 }
66
67 claims := jwt.Claims{}
68 if err = parsed.Claims(key.PublicKey, &claims); err != nil {
69 return nil, err
70 }
71
72 if err = claims.Validate(jwt.Expected{
73 Audience: jwt.Audience{m.Audience},
74 Time: time.Now(), // +/- 1 minute
75 }); err != nil {
76 return nil, err
77 }
78
79 // If we made it here then the user matches the public key used to sign the
80 // token and the claims are verified so just return the user record we
81 // fetched earlier.
82
83 return user, nil
84}
85
86// CreateForUser creates a new JWT based on the passed user. In some cases it
87// may also need to generate a new SessionKey for the purpose of encrypting
88// those JWTs. Callers should be sure to save the returned SessionKey, if there
89// is one, to the user when setting the token otherwise the token signature
90// will not be able to be validated on subsequent requests.
91func (m *JWTManager) CreateForUser(u *models.User) (string, *models.SessionKey, error) {
92 pk, err := models.GenerateSessionKey(m.TokenExpires)
93 if err != nil {
94 return "", nil, err
95 }
96
97 // The ExtraHeaders bit is kind of an ugly hack but there's no way in the
98 // official API to set the KeyID for single-signer tokens. However, if the
99 // magic "kid" label is set in the extra headers it will be set as the
100 // KeyID in the header when that's deserialized.
101 signer, err := jose.NewSigner(jose.SigningKey{
102 Algorithm: jose.ES256,
103 Key: pk.PrivateKey,
104 }, &jose.SignerOptions{
105 ExtraHeaders: map[jose.HeaderKey]interface{}{
106 keyIdHeader: pk.KeyId,
107 },
108 })
109 if err != nil {
110 return "", nil, err
111 }
112
113 now := time.Now()
114 j, err := jwt.Signed(signer).Claims(jwt.Claims{
115 Issuer: m.Audience,
116 Subject: u.Username,
117 Audience: jwt.Audience{m.Audience},
118 Expiry: jwt.NewNumericDate(now.Add(m.TokenExpires)),
119 IssuedAt: jwt.NewNumericDate(now),
120 }).CompactSerialize()
121
122 return j, pk, err
123}