forked from vmware-tanzu/tanzu-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
/
selfmanaged.go
196 lines (178 loc) · 6.59 KB
/
selfmanaged.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
188
189
190
191
192
193
194
195
196
// Copyright 2023 VMware, Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package csp
import (
"context"
"fmt"
"net/http"
"net/url"
"os"
"os/signal"
"sync"
"time"
"github.com/pkg/errors"
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
"go.pinniped.dev/pkg/oidcclient/pkce"
"go.pinniped.dev/pkg/oidcclient/state"
"golang.org/x/oauth2"
"golang.org/x/sync/errgroup"
"github.com/vmware-tanzu/tanzu-plugin-runtime/log"
)
const (
tokenEndpointSuffix = "oauth2/token" //nolint:gosec
authorizationEndpointSuffix = "oauth2/authorize"
redirectURL = "http://127.0.0.1/callback"
// pinniped-cli is a special pinniped-supervisor client ID that has http://127.0.0.1/callback as the
// only allowed Redirect URI and does not have an associated client secret.
pinnipedCLIClientID = oidcapi.ClientIDPinnipedCLI
loginScopes = "openid offline_access username groups"
// PinnipedSupervisorDomain is the domain name for the pinniped supervisor token issuer that is
// deployed in TMC self-managed environment to serve as an identity broker.
PinnipedSupervisorDomain = "pinniped-supervisor"
// FederationDomainPath is the path in the issuer URL of the federation domain setup to work
// with the upstream identity provider.
// TODO(ashisham): finalize what the federation domain deployed in production will look like
// Prod and non-prod environments can share this path as the DNS zone from which the issuer URL
// is generated from will be different.
FederationDomainPath = "provider/pinniped"
)
var (
// making the context and the cancel function used for token exchange
// accessible in the callback handler to have the local listener
// gracefully shutdown once token exchange is completed.
tokenExchange context.Context
tokenExchangeComplete context.CancelFunc
token *oauth2.Token
// share a common oauth config for the package as this is the only
// oauth2 config used for the self-managed auth flows.
sharedOauthConfig *oauth2.Config
// pkce code instance to generate the challenge and verifier code pair.
pkceCodePair pkce.Code
)
func callbackHandler(w http.ResponseWriter, r *http.Request) {
// token exchange should be complete once this callback handler completes execution.
defer tokenExchangeComplete()
code := r.URL.Query().Get("code")
if code == "" {
errMsg := fmt.Sprintf("[state] query params is required, URL %s did not have this query parameters", r.URL.String())
http.Error(w, errMsg, http.StatusBadRequest)
log.Info(errMsg)
return
}
var err error
token, err = sharedOauthConfig.Exchange(tokenExchange, code, pkceCodePair.Verifier())
if err != nil {
errMsg := fmt.Sprintf("failed to exchange auth code for oauth tokens, err=%v", err)
http.Error(w, errMsg, http.StatusInternalServerError)
log.Info(errMsg)
return
}
fmt.Fprint(w, "You have successfully logged in! You can now safely close this window")
}
func interruptHandler(d context.CancelFunc) {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
for range c {
d()
log.Fatal(nil, "login flow interrupted")
}
}
// runLocalListener is a blocking function call that starts a local listener
// to handle auth-code flow callback to perform token exchange.
func runLocalListener() error {
mux := http.NewServeMux()
mux.HandleFunc("/callback", callbackHandler)
tokenExchange, tokenExchangeComplete = context.WithCancel(context.TODO())
//nolint:gosec
l := http.Server{
Addr: "",
Handler: mux,
}
// run a go routine to catch interrupt signals from the CLI to
// gracefully shutdown the local listener
go interruptHandler(tokenExchangeComplete)
// run a go routine to shut down the local listener once token
// exchange is completed
go func() {
<-tokenExchange.Done()
_ = l.Shutdown(tokenExchange)
}()
if err := l.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return errors.Wrapf(err, "failed to run a local listener to facilitate login")
}
return nil
}
func getAuthCodeURL() (string, error) {
stateVal, err := state.Generate()
if err != nil {
return "", errors.Wrap(err, "failed to generate state parameter")
}
opts := []oauth2.AuthCodeOption{
pkceCodePair.Challenge(),
pkceCodePair.Method(),
}
return sharedOauthConfig.AuthCodeURL(stateVal.String(), opts...), nil
}
func GetAccessTokenFromSelfManagedIDP(refreshToken, issuerURL string) (*Token, error) {
var mutex sync.Mutex
mutex.Lock()
defer mutex.Unlock()
sharedOauthConfig = &oauth2.Config{
RedirectURL: redirectURL,
ClientID: pinnipedCLIClientID,
ClientSecret: "",
Scopes: []string{"openid", "offline_access", "username", "groups"},
Endpoint: oauth2.Endpoint{
AuthURL: fmt.Sprintf("%s/%s", issuerURL, authorizationEndpointSuffix),
TokenURL: fmt.Sprintf("%s/%s", issuerURL, tokenEndpointSuffix),
},
}
if refreshToken != "" {
refreshedToken, err := sharedOauthConfig.TokenSource(context.TODO(), &oauth2.Token{RefreshToken: refreshToken}).Token()
if err == nil {
return &Token{
IDToken: refreshedToken.Extra("id_token").(string),
AccessToken: refreshedToken.AccessToken,
RefreshToken: refreshedToken.RefreshToken,
ExpiresIn: int64(time.Until(refreshedToken.Expiry).Seconds()),
Scope: loginScopes,
TokenType: "id_token",
}, nil
}
log.Infof("failed to refresh token, err %v", err)
// proceed with login flow through the browser
}
// set the issuer package variable to be used in the callback handler
if _, err := url.Parse(issuerURL); err != nil {
return nil, errors.Errorf("Issuer URL [%s] is not a valid URL", issuerURL)
}
var err error
if pkceCodePair, err = pkce.Generate(); err != nil {
return nil, errors.Wrapf(err, "failed to generate pkce code pair generator")
}
// perform a browser based login flow if no refreshToken was supplied
// or if token refresh failed.
g := &errgroup.Group{}
g.Go(
runLocalListener,
)
authCodeURL, err := getAuthCodeURL()
if err != nil {
return nil, errors.Wrapf(err, "failed to generate authcode url for OIDC provider at %s, err= %v", sharedOauthConfig.Endpoint.AuthURL, err)
}
fmt.Printf("Please open this URL in a browser window to complete the login\n\t %s\n", authCodeURL)
if err := g.Wait(); err != nil {
return nil, err
}
if token == nil || token.Extra("id_token").(string) == "" {
return nil, errors.Errorf("token issuer %s did not return expected tokens", issuerURL)
}
return &Token{
IDToken: token.Extra("id_token").(string),
AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
ExpiresIn: int64(time.Until(token.Expiry).Seconds()),
Scope: loginScopes,
TokenType: "id_token",
}, nil
}