blob: 20c40af1665bc9ce189159a0f8258d35049a5772 [file] [log] [blame]
Jiri Simsa5293dcb2014-05-10 09:56:38 -07001// HTTP server that uses OAuth to create security.PrivateID objects.
2package main
3
4import (
5 "flag"
6 "fmt"
Asim Shankar71061572014-07-22 16:59:18 -07007 "html/template"
Robin Thellend37036802014-06-03 17:40:23 -07008 "net"
Jiri Simsa5293dcb2014-05-10 09:56:38 -07009 "net/http"
10 "os"
Suharsh Sivakumarfb5cbb72014-08-27 13:14:22 -070011 "path/filepath"
Jiri Simsa5293dcb2014-05-10 09:56:38 -070012 "strings"
Asim Shankar71061572014-07-22 16:59:18 -070013 "time"
Jiri Simsa5293dcb2014-05-10 09:56:38 -070014
Asim Shankar61071792014-07-22 13:03:18 -070015 "veyron/lib/signals"
Tilak Sharma3ed30242014-08-11 11:45:55 -070016 vsecurity "veyron/security"
Asim Shankar3afe7902014-08-12 11:43:48 -070017 "veyron/security/audit"
18 "veyron/services/identity/auditor"
Asim Shankar61071792014-07-22 13:03:18 -070019 "veyron/services/identity/blesser"
Jiri Simsa5293dcb2014-05-10 09:56:38 -070020 "veyron/services/identity/googleoauth"
21 "veyron/services/identity/handlers"
Suharsh Sivakumarfb5cbb72014-08-27 13:14:22 -070022 "veyron/services/identity/revocation"
23 services "veyron/services/security"
24 "veyron/services/security/discharger"
Tilak Sharma3ed30242014-08-11 11:45:55 -070025
Asim Shankar61071792014-07-22 13:03:18 -070026 "veyron2"
27 "veyron2/ipc"
Asim Shankar3afe7902014-08-12 11:43:48 -070028 "veyron2/naming"
Jiri Simsa5293dcb2014-05-10 09:56:38 -070029 "veyron2/rt"
30 "veyron2/security"
31 "veyron2/vlog"
32)
33
34var (
Asim Shankar61071792014-07-22 13:03:18 -070035 httpaddr = flag.String("httpaddr", "localhost:8125", "Address on which the HTTP server listens on.")
36 tlsconfig = flag.String("tlsconfig", "", "Comma-separated list of TLS certificate and private key files. If empty, will not use HTTPS.")
37 // TODO(ashankar): Revisit the choices for -vaddr and -vprotocol once the proxy design in relation to mounttables has been finalized.
38 address = flag.String("vaddr", "proxy.envyor.com:8100", "Address on which the Veyron blessing server listens on. Enabled iff --google_config is set")
39 protocol = flag.String("vprotocol", "veyron", "Protocol used to interpret --vaddr")
Jiri Simsa5293dcb2014-05-10 09:56:38 -070040 host = flag.String("host", defaultHost(), "Hostname the HTTP server listens on. This can be the name of the host running the webserver, but if running behind a NAT or load balancer, this should be the host name that clients will connect to. For example, if set to 'x.com', Veyron identities will have the IssuerName set to 'x.com' and clients can expect to find the public key of the signer at 'x.com/pubkey/'.")
Jiri Simsa5293dcb2014-05-10 09:56:38 -070041 minExpiryDays = flag.Int("min_expiry_days", 365, "Minimum expiry time (in days) of identities issued by this server")
Asim Shankar71061572014-07-22 16:59:18 -070042
Asim Shankar3afe7902014-08-12 11:43:48 -070043 auditprefix = flag.String("audit", "", "File prefix to files where auditing information will be written.")
44 auditfilter = flag.String("audit_filter", "", "If non-empty, instead of starting the server the audit log will be dumped to STDOUT (with the filter set to the value of this flag. '/' can be used to dump all events).")
45
Asim Shankar7a721752014-08-02 14:27:23 -070046 // Configuration for various Google OAuth-based clients.
Asim Shankar3afe7902014-08-12 11:43:48 -070047 googleConfigWeb = flag.String("google_config_web", "", "Path to JSON-encoded OAuth client configuration for the web application that renders the audit log for blessings provided by this provider.")
Asim Shankar7a721752014-08-02 14:27:23 -070048 googleConfigInstalled = flag.String("google_config_installed", "", "Path to the JSON-encoded OAuth client configuration for installed client applications that obtain blessings (via the OAuthBlesser.BlessUsingAuthorizationCode RPC) from this server (like the 'identity' command like tool and its 'seekblessing' command.")
49 googleConfigChrome = flag.String("google_config_chrome", "", "Path to the JSON-encoded OAuth client configuration for Chrome browser applications that obtain blessings from this server (via the OAuthBlesser.BlessUsingAccessToken RPC) from this server.")
Asim Shankar1c3b1812014-07-31 18:54:51 -070050 googleDomain = flag.String("google_domain", "", "An optional domain name. When set, only email addresses from this domain are allowed to authenticate via Google OAuth")
Suharsh Sivakumarfb5cbb72014-08-27 13:14:22 -070051
52 // Revoker/Discharger configuration
53 revocationDir = flag.String("revocation_dir", filepath.Join(os.TempDir(), "revocation_dir"), "Path where the revocation manager will store caveat and revocation information.")
Jiri Simsa5293dcb2014-05-10 09:56:38 -070054)
55
56func main() {
Jiri Simsa5293dcb2014-05-10 09:56:38 -070057 flag.Usage = usage
Asim Shankar3afe7902014-08-12 11:43:48 -070058 r := rt.Init(providerIdentity())
Bogdan Caprita4258d882014-07-02 09:15:22 -070059 defer r.Cleanup()
Jiri Simsa5293dcb2014-05-10 09:56:38 -070060
Asim Shankar3afe7902014-08-12 11:43:48 -070061 if len(*auditfilter) > 0 {
62 dumpAuditLog()
63 return
64 }
Suharsh Sivakumarfb5cbb72014-08-27 13:14:22 -070065
66 // Calling with empty string returns a empty RevocationManager
67 revocationManager, err := revocation.NewRevocationManager(*revocationDir)
68 if err != nil {
69 vlog.Fatalf("Failed to start RevocationManager: %v", err)
70 }
71
Jiri Simsa5293dcb2014-05-10 09:56:38 -070072 // Setup handlers
Jiri Simsa5293dcb2014-05-10 09:56:38 -070073 http.Handle("/pubkey/", handlers.Object{r.Identity().PublicID().PublicKey()}) // public key of this identity server
Robin Thellendb6406092014-05-12 16:38:58 -070074 if enableRandomHandler() {
75 http.Handle("/random/", handlers.Random{r}) // mint identities with a random name
76 }
77 http.HandleFunc("/bless/", handlers.Bless) // use a provided PrivateID to bless a provided PublicID
Asim Shankar61071792014-07-22 13:03:18 -070078
Jiri Simsa5293dcb2014-05-10 09:56:38 -070079 // Google OAuth
Suharsh Sivakumarfb5cbb72014-08-27 13:14:22 -070080 ipcServer, ipcServerEP, err := setupGoogleBlessingDischargingServer(r, revocationManager)
Asim Shankar7a721752014-08-02 14:27:23 -070081 if err != nil {
82 vlog.Fatalf("Failed to setup veyron services for blessing: %v", err)
83 }
84 if ipcServer != nil {
Asim Shankar71061572014-07-22 16:59:18 -070085 defer ipcServer.Stop()
86 }
Asim Shankar3afe7902014-08-12 11:43:48 -070087 if enabled, clientID, clientSecret := enableGoogleOAuth(*googleConfigWeb); enabled && len(*auditprefix) > 0 {
Jiri Simsa5293dcb2014-05-10 09:56:38 -070088 n := "/google/"
89 http.Handle(n, googleoauth.NewHandler(googleoauth.HandlerArgs{
Suharsh Sivakumar5ee0ee62014-09-04 13:16:34 -070090 Addr: fmt.Sprintf("%s%s", httpaddress(), n),
91 ClientID: clientID,
92 ClientSecret: clientSecret,
93 Auditor: *auditprefix,
94 RevocationManager: revocationManager,
Jiri Simsa5293dcb2014-05-10 09:56:38 -070095 }))
96 }
Asim Shankar71061572014-07-22 16:59:18 -070097 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
98 var servers []string
99 if ipcServer != nil {
100 servers, _ = ipcServer.Published()
101 }
Asim Shankar3afe7902014-08-12 11:43:48 -0700102 if len(servers) == 0 {
103 // No addresses published, publish the endpoint instead (which may not be usable everywhere, but oh-well).
Suharsh Sivakumarfb5cbb72014-08-27 13:14:22 -0700104 servers = append(servers, ipcServerEP.String())
Asim Shankar3afe7902014-08-12 11:43:48 -0700105 }
Asim Shankar71061572014-07-22 16:59:18 -0700106 args := struct {
Suharsh Sivakumarfb5cbb72014-08-27 13:14:22 -0700107 Self string
108 GoogleWeb, RandomWeb bool
109 GoogleServers, DischargeServers []string
Asim Shankar71061572014-07-22 16:59:18 -0700110 }{
Suharsh Sivakumarfb5cbb72014-08-27 13:14:22 -0700111 Self: rt.R().Identity().PublicID().Names()[0],
112 GoogleWeb: len(*googleConfigWeb) > 0,
113 RandomWeb: enableRandomHandler(),
114 GoogleServers: appendSuffixTo(servers, "google"),
115 DischargeServers: appendSuffixTo(servers, "discharger"),
Asim Shankar71061572014-07-22 16:59:18 -0700116 }
117 if err := tmpl.Execute(w, args); err != nil {
118 vlog.Info("Failed to render template:", err)
119 }
120 })
Asim Shankar1c3b1812014-07-31 18:54:51 -0700121 vlog.Infof("Running HTTP server at: %v", httpaddress())
Asim Shankar61071792014-07-22 13:03:18 -0700122 go runHTTPServer(*httpaddr)
123 <-signals.ShutdownOnSignals()
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700124}
125
Suharsh Sivakumarfb5cbb72014-08-27 13:14:22 -0700126func appendSuffixTo(objectname []string, suffix string) []string {
127 names := make([]string, len(objectname))
128 for i, o := range objectname {
129 names[i] = naming.JoinAddressName(o, suffix)
130 }
131 return names
132}
133
134// newDispatcher returns a dispatcher for both the blessing and the discharging service.
135// their suffix. ReflectInvoker is used to invoke methods.
136func newDispatcher(params blesser.GoogleParams) ipc.Dispatcher {
137 blessingService := ipc.ReflectInvoker(blesser.NewGoogleOAuthBlesserServer(params))
138 dischargerService := ipc.ReflectInvoker(services.NewServerDischarger(discharger.NewDischarger(params.R.Identity())))
Asim Shankar9f6db082014-08-27 16:44:03 -0700139 allowEveryoneACLAuth := vsecurity.NewACLAuthorizer(security.ACL{In: map[security.BlessingPattern]security.LabelSet{
Suharsh Sivakumarfb5cbb72014-08-27 13:14:22 -0700140 security.AllPrincipals: security.AllLabels,
Asim Shankar9f6db082014-08-27 16:44:03 -0700141 }})
Suharsh Sivakumarfb5cbb72014-08-27 13:14:22 -0700142 return &dispatcher{blessingService, dischargerService, allowEveryoneACLAuth}
143}
144
145type dispatcher struct {
146 blessingInvoker, dischargerInvoker ipc.Invoker
147 auth security.Authorizer
148}
149
150func (d dispatcher) Lookup(suffix, method string) (ipc.Invoker, security.Authorizer, error) {
151 switch suffix {
152 case "google":
153 return d.blessingInvoker, d.auth, nil
154 case "discharger":
155 return d.dischargerInvoker, d.auth, nil
156 default:
157 return nil, nil, fmt.Errorf("suffix does not exist")
158 }
159}
160
161// Starts the blessing service and the discharging service on the same port.
162func setupGoogleBlessingDischargingServer(r veyron2.Runtime, revocationManager *revocation.RevocationManager) (ipc.Server, naming.Endpoint, error) {
Asim Shankar7a721752014-08-02 14:27:23 -0700163 var enable bool
164 params := blesser.GoogleParams{
165 R: r,
166 BlessingDuration: time.Duration(*minExpiryDays) * 24 * time.Hour,
167 DomainRestriction: *googleDomain,
Suharsh Sivakumarfb5cbb72014-08-27 13:14:22 -0700168 RevocationManager: revocationManager,
Asim Shankar7a721752014-08-02 14:27:23 -0700169 }
170 if authcode, clientID, clientSecret := enableGoogleOAuth(*googleConfigInstalled); authcode {
171 enable = true
172 params.AuthorizationCodeClient.ID = clientID
173 params.AuthorizationCodeClient.Secret = clientSecret
174 }
175 if accesstoken, clientID, _ := enableGoogleOAuth(*googleConfigChrome); accesstoken {
176 enable = true
177 params.AccessTokenClient.ID = clientID
178 }
179 if !enable {
Asim Shankar3afe7902014-08-12 11:43:48 -0700180 return nil, nil, nil
Asim Shankar7a721752014-08-02 14:27:23 -0700181 }
Asim Shankar61071792014-07-22 13:03:18 -0700182 server, err := r.NewServer()
183 if err != nil {
Asim Shankar3afe7902014-08-12 11:43:48 -0700184 return nil, nil, fmt.Errorf("failed to create new ipc.Server: %v", err)
Asim Shankar61071792014-07-22 13:03:18 -0700185 }
186 ep, err := server.Listen(*protocol, *address)
187 if err != nil {
Asim Shankar3afe7902014-08-12 11:43:48 -0700188 return nil, nil, fmt.Errorf("server.Listen(%q, %q) failed: %v", "tcp", *address, err)
Asim Shankar61071792014-07-22 13:03:18 -0700189 }
Suharsh Sivakumarfb5cbb72014-08-27 13:14:22 -0700190 params.DischargerLocation = naming.JoinAddressName(ep.String(), "discharger")
191 dispatcher := newDispatcher(params)
192 objectname := fmt.Sprintf("identity/%s", r.Identity().PublicID().Names()[0])
193 if err := server.Serve(objectname, dispatcher); err != nil {
194 return nil, nil, fmt.Errorf("failed to start Veyron services: %v", err)
Asim Shankar61071792014-07-22 13:03:18 -0700195 }
Suharsh Sivakumarfb5cbb72014-08-27 13:14:22 -0700196 vlog.Infof("Google blessing and discharger services enabled at endpoint %v and name %q", ep, objectname)
Asim Shankar3afe7902014-08-12 11:43:48 -0700197 return server, ep, nil
Asim Shankar61071792014-07-22 13:03:18 -0700198}
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700199
Asim Shankar3afe7902014-08-12 11:43:48 -0700200func enableTLS() bool { return len(*tlsconfig) > 0 }
201func enableRandomHandler() bool {
202 return len(*googleConfigInstalled)+len(*googleConfigWeb)+len(*googleConfigChrome) == 0
203}
Asim Shankar71061572014-07-22 16:59:18 -0700204func enableGoogleOAuth(config string) (enabled bool, clientID, clientSecret string) {
205 fname := config
Asim Shankar61071792014-07-22 13:03:18 -0700206 if len(fname) == 0 {
207 return false, "", ""
208 }
209 f, err := os.Open(fname)
210 if err != nil {
211 vlog.Fatalf("Failed to open %q: %v", fname, err)
212 }
213 defer f.Close()
214 clientID, clientSecret, err = googleoauth.ClientIDAndSecretFromJSON(f)
215 if err != nil {
216 vlog.Fatalf("Failed to decode JSON in %q: %v", fname, err)
217 }
218 return true, clientID, clientSecret
219}
220
221func runHTTPServer(addr string) {
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700222 if !enableTLS() {
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700223 if err := http.ListenAndServe(addr, nil); err != nil {
224 vlog.Fatalf("http.ListenAndServe failed: %v", err)
225 }
226 return
227 }
228 paths := strings.Split(*tlsconfig, ",")
229 if len(paths) != 2 {
230 vlog.Fatalf("Could not parse --tlsconfig. Must have exactly two components, separated by a comma")
231 }
232 vlog.Infof("Starting HTTP server with TLS using certificate [%s] and private key [%s] at https://%s", paths[0], paths[1], addr)
233 if err := http.ListenAndServeTLS(addr, paths[0], paths[1], nil); err != nil {
234 vlog.Fatalf("http.ListenAndServeTLS failed: %v", err)
235 }
236}
237
238func usage() {
239 fmt.Fprintf(os.Stderr, `%s starts an HTTP server that mints veyron identities in response to GET requests.
240
241To generate TLS certificates so the HTTP server can use SSL:
242go run $GOROOT/src/pkg/crypto/tls/generate_cert.go --host <IP address>
243
Asim Shankar1c3b1812014-07-31 18:54:51 -0700244To generate an identity for this service itself, use:
245go install veyron/tools/identity && ./bin/identity generate <name> ><filename>
246and set the VEYRON_IDENTITY environment variable when running this application.
247
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700248To enable use of Google APIs to use Google OAuth for authorization, set --google_config,
249which must point to the contents of a JSON file obtained after registering your application
250with the Google Developer Console at:
251https://code.google.com/apis/console
252More details on Google OAuth at:
253https://developers.google.com/accounts/docs/OAuth2Login
254
255Flags:
256`, os.Args[0])
257 flag.PrintDefaults()
258}
259
260func defaultHost() string {
261 host, err := os.Hostname()
262 if err != nil {
263 vlog.Fatalf("Failed to get hostname: %v", err)
264 }
265 return host
266}
267
Asim Shankar3afe7902014-08-12 11:43:48 -0700268// providerIdentity returns the identity of the identity provider (i.e., this program) itself.
269func providerIdentity() veyron2.ROpt {
270 // TODO(ashankar): This scheme of initializing a runtime just to share the "load identity" code is ridiculous.
271 // Figure out a way to update the runtime's identity with a wrapper and avoid this spurios "New" call.
272 r, err := rt.New()
273 if err != nil {
274 vlog.Fatal(err)
275 }
276 defer r.Cleanup()
277 id := r.Identity()
278 if len(*auditprefix) > 0 {
279 auditor, err := auditor.NewFileAuditor(*auditprefix)
280 if err != nil {
281 vlog.Fatal(err)
282 }
283 id = audit.NewPrivateID(id, auditor)
284 }
285 return veyron2.RuntimeID(id)
286}
287
Asim Shankar1c3b1812014-07-31 18:54:51 -0700288func httpaddress() string {
289 _, port, err := net.SplitHostPort(*httpaddr)
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700290 if err != nil {
Asim Shankar1c3b1812014-07-31 18:54:51 -0700291 vlog.Fatalf("Failed to parse %q: %v", *httpaddr, err)
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700292 }
Asim Shankar1c3b1812014-07-31 18:54:51 -0700293 scheme := "http"
294 if enableTLS() {
295 scheme = "https"
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700296 }
Asim Shankar1c3b1812014-07-31 18:54:51 -0700297 return fmt.Sprintf("%s://%s:%v", scheme, *host, port)
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700298}
Asim Shankar71061572014-07-22 16:59:18 -0700299
Asim Shankar3afe7902014-08-12 11:43:48 -0700300func dumpAuditLog() {
301 if len(*auditprefix) == 0 {
302 vlog.Fatalf("Must set --audit")
303 }
304 ch, err := auditor.ReadAuditLog(*auditprefix, *auditfilter)
305 if err != nil {
306 vlog.Fatal(err)
307 }
308 idx := 0
309 for entry := range ch {
310 fmt.Printf("%6d) %v\n", idx, entry)
311 idx++
312 }
313}
314
Asim Shankar71061572014-07-22 16:59:18 -0700315var tmpl = template.Must(template.New("main").Parse(`<!doctype html>
316<html>
317<head>
318<meta charset="UTF-8">
319<title>Veyron Identity Server</title>
320<meta name="viewport" content="width=device-width, initial-scale=1.0">
321<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
322</head>
323<body>
324<div class="container">
Asim Shankar3afe7902014-08-12 11:43:48 -0700325<div class="page-header"><h2>{{.Self}}</h2><h4>A Veyron Identity Provider</h4></div>
Asim Shankar71061572014-07-22 16:59:18 -0700326<div class="well">
Asim Shankar3afe7902014-08-12 11:43:48 -0700327This is a Veyron identity provider that provides blessings with the name prefix <mark>{{.Self}}</mark>. The public
328key of this provider is available in <a class="btn btn-xs btn-primary" href="/pubkey/base64vom">base64-encoded-vom-encoded</a> format.
Asim Shankar71061572014-07-22 16:59:18 -0700329</div>
Asim Shankar3afe7902014-08-12 11:43:48 -0700330
Asim Shankar71061572014-07-22 16:59:18 -0700331{{if .GoogleServers}}
332<div class="well">
Asim Shankar3afe7902014-08-12 11:43:48 -0700333Blessings are provided via Veyron RPCs to: <tt>{{range .GoogleServers}}{{.}}{{end}}</tt>
Asim Shankar71061572014-07-22 16:59:18 -0700334</div>
335{{end}}
Suharsh Sivakumarfb5cbb72014-08-27 13:14:22 -0700336{{if .DischargeServers}}
337<div class="well">
338RevocationCaveat Discharges are provided via Veyron RPCs to: <tt>{{range .DischargeServers}}{{.}}{{end}}</tt>
339</div>
340{{end}}
341
Asim Shankar71061572014-07-22 16:59:18 -0700342
343{{if .GoogleWeb}}
Asim Shankar3afe7902014-08-12 11:43:48 -0700344<div class="well">
345This page provides the ability to <a class="btn btn-xs btn-primary" href="/google/auth">enumerate</a> blessings provided with your
346email address as the name.
347</div>
Asim Shankar71061572014-07-22 16:59:18 -0700348{{end}}
Asim Shankar3afe7902014-08-12 11:43:48 -0700349
Asim Shankar71061572014-07-22 16:59:18 -0700350{{if .RandomWeb}}
Asim Shankar3afe7902014-08-12 11:43:48 -0700351<div class="well">
352You can obtain a randomly assigned PrivateID <a class="btn btn-sm btn-primary" href="/random/">here</a>
353</div>
Asim Shankar71061572014-07-22 16:59:18 -0700354{{end}}
Asim Shankar3afe7902014-08-12 11:43:48 -0700355
356<div class="well">
357You can use <a class="btn btn-xs btn-primary" href="/bless/">this form</a> to offload crypto for blessing to this HTTP server
358</div>
Asim Shankar71061572014-07-22 16:59:18 -0700359
360</div>
361</body>
362</html>`))