blob: 4294c7147a9a68e0876ebcd266cd89d616c9ba08 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// HTTP server that uses OAuth to create security.Blessings objects.
package server
import (
"crypto/rand"
"fmt"
mrand "math/rand"
"net"
"net/http"
"reflect"
"strconv"
"strings"
"syscall"
"time"
"v.io/v23"
"v.io/v23/context"
"v.io/v23/naming"
"v.io/v23/rpc"
"v.io/v23/security"
"v.io/v23/verror"
"v.io/x/lib/vlog"
"v.io/x/ref/lib/security/audit"
"v.io/x/ref/lib/signals"
"v.io/x/ref/services/discharger"
"v.io/x/ref/services/identity/internal/auditor"
"v.io/x/ref/services/identity/internal/blesser"
"v.io/x/ref/services/identity/internal/caveats"
"v.io/x/ref/services/identity/internal/dischargerlib"
"v.io/x/ref/services/identity/internal/handlers"
"v.io/x/ref/services/identity/internal/oauth"
"v.io/x/ref/services/identity/internal/revocation"
"v.io/x/ref/services/identity/internal/templates"
)
const (
// TODO(ataly, ashankar, suharshs): The name "google" for the oauthBlesserService does
// not seem appropriate given our modular construction of the identity server. The
// oauthBlesserService can use any oauthProvider of its choosing, i.e., it does not
// always have to be "google". One option would be change the value to "oauth". This
// would also make the name analogous to that of macaroonService. Note that this option
// also requires changing the extension.
oauthBlesserService = "google"
macaroonService = "macaroon"
dischargerService = "discharger"
)
type IdentityServer struct {
oauthProvider oauth.OAuthProvider
auditor audit.Auditor
blessingLogReader auditor.BlessingLogReader
revocationManager revocation.RevocationManager
oauthBlesserParams blesser.OAuthBlesserParams
caveatSelector caveats.CaveatSelector
rootedObjectAddrs []naming.Endpoint
assetsPrefix string
mountNamePrefix string
}
// NewIdentityServer returns a IdentityServer that:
// - uses oauthProvider to authenticate users
// - auditor and blessingLogReader to audit the root principal and read audit logs
// - revocationManager to store revocation data and grant discharges
// - oauthBlesserParams to configure the identity.OAuthBlesser service
func NewIdentityServer(oauthProvider oauth.OAuthProvider, auditor audit.Auditor, blessingLogReader auditor.BlessingLogReader, revocationManager revocation.RevocationManager, oauthBlesserParams blesser.OAuthBlesserParams, caveatSelector caveats.CaveatSelector, assetsPrefix, mountNamePrefix string) *IdentityServer {
return &IdentityServer{
oauthProvider: oauthProvider,
auditor: auditor,
blessingLogReader: blessingLogReader,
revocationManager: revocationManager,
oauthBlesserParams: oauthBlesserParams,
caveatSelector: caveatSelector,
assetsPrefix: assetsPrefix,
mountNamePrefix: mountNamePrefix,
}
}
// findUnusedPort finds an unused port and returns it. Of course, no guarantees
// are made that the port will actually be available by the time the caller
// gets around to binding to it. If no port can be found, (0, nil) is returned.
// If an error occurs while creating a socket, that error is returned and the
// other return value is 0.
func findUnusedPort() (int, error) {
random := mrand.New(mrand.NewSource(time.Now().UnixNano()))
for i := 0; i < 1000; i++ {
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP)
if err != nil {
return 0, err
}
port := int(1024 + random.Int31n(64512))
sa := &syscall.SockaddrInet4{Port: port}
err = syscall.Bind(fd, sa)
syscall.Close(fd)
if err == nil {
return port, nil
}
}
return 0, nil
}
func (s *IdentityServer) Serve(ctx *context.T, listenSpec *rpc.ListenSpec, externalHttpAddr, httpAddr, tlsConfig string) {
ctx, err := v23.WithPrincipal(ctx, audit.NewPrincipal(
v23.GetPrincipal(ctx), s.auditor))
if err != nil {
vlog.Panic(err)
}
httphost, httpport, err := net.SplitHostPort(httpAddr)
if err != nil || httpport == "0" {
httpportNum, err := findUnusedPort()
if err != nil {
vlog.Panic(err)
}
httpAddr = net.JoinHostPort(httphost, strconv.Itoa(httpportNum))
}
rpcServer, _, externalAddr := s.Listen(ctx, listenSpec, externalHttpAddr, httpAddr, tlsConfig)
fmt.Printf("HTTP_ADDR=%s\n", externalAddr)
if len(s.rootedObjectAddrs) > 0 {
fmt.Printf("NAME=%s\n", s.rootedObjectAddrs[0].Name())
}
<-signals.ShutdownOnSignals(ctx)
if err := rpcServer.Stop(); err != nil {
vlog.Errorf("Failed to stop rpc server: %v", err)
}
}
func (s *IdentityServer) Listen(ctx *context.T, listenSpec *rpc.ListenSpec, externalHttpAddr, httpAddr, tlsConfig string) (rpc.Server, []string, string) {
// Setup handlers
// json-encoded public key and blessing names of this server
principal := v23.GetPrincipal(ctx)
http.Handle("/auth/blessing-root", handlers.BlessingRoot{principal})
macaroonKey := make([]byte, 32)
if _, err := rand.Read(macaroonKey); err != nil {
vlog.Fatalf("macaroonKey generation failed: %v", err)
}
rpcServer, published, err := s.setupServices(ctx, listenSpec, macaroonKey)
if err != nil {
vlog.Fatalf("Failed to setup vanadium services for blessing: %v", err)
}
externalHttpAddr = httpAddress(externalHttpAddr, httpAddr)
http.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
n := "/auth/google/"
args := oauth.HandlerArgs{
Principal: principal,
MacaroonKey: macaroonKey,
Addr: fmt.Sprintf("%s%s", externalHttpAddr, n),
BlessingLogReader: s.blessingLogReader,
RevocationManager: s.revocationManager,
DischargerLocation: naming.JoinAddressName(published[0], dischargerService),
MacaroonBlessingService: naming.JoinAddressName(published[0], macaroonService),
OAuthProvider: s.oauthProvider,
CaveatSelector: s.caveatSelector,
AssetsPrefix: s.assetsPrefix,
}
if s.revocationManager != nil {
args.DischargeServers = appendSuffixTo(published, dischargerService)
}
var emptyParams blesser.OAuthBlesserParams
if !reflect.DeepEqual(s.oauthBlesserParams, emptyParams) {
args.GoogleServers = appendSuffixTo(published, oauthBlesserService)
}
h, err := oauth.NewHandler(args)
if err != nil {
vlog.Fatalf("Failed to create HTTP handler for oauth authentication: %v", err)
}
http.Handle(n, h)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tmplArgs := struct {
Self security.Blessings
GoogleServers, DischargeServers []string
ListBlessingsRoute string
AssetsPrefix string
Email string
}{
Self: principal.BlessingStore().Default(),
GoogleServers: args.GoogleServers,
DischargeServers: args.DischargeServers,
ListBlessingsRoute: oauth.ListBlessingsRoute,
AssetsPrefix: s.assetsPrefix,
}
if err := templates.Home.Execute(w, tmplArgs); err != nil {
vlog.Info("Failed to render template:", err)
}
})
vlog.Infof("Running HTTP server at: %v", externalHttpAddr)
go runHTTPSServer(httpAddr, tlsConfig)
return rpcServer, published, externalHttpAddr
}
func appendSuffixTo(objectname []string, suffix string) []string {
names := make([]string, len(objectname))
for i, o := range objectname {
names[i] = naming.JoinAddressName(o, suffix)
}
return names
}
// Starts the blessing services and the discharging service on the same port.
func (s *IdentityServer) setupServices(ctx *context.T, listenSpec *rpc.ListenSpec, macaroonKey []byte) (rpc.Server, []string, error) {
server, err := v23.NewServer(ctx)
if err != nil {
return nil, nil, fmt.Errorf("failed to create new rpc.Server: %v", err)
}
principal := v23.GetPrincipal(ctx)
objectAddr := naming.Join(s.mountNamePrefix, fmt.Sprintf("%v", principal.BlessingStore().Default()))
if s.rootedObjectAddrs, err = server.Listen(*listenSpec); err != nil {
defer server.Stop()
return nil, nil, fmt.Errorf("server.Listen(%v) failed: %v", *listenSpec, err)
}
var rootedObjectAddr string
if naming.Rooted(objectAddr) {
rootedObjectAddr = objectAddr
} else if nsroots := v23.GetNamespace(ctx).Roots(); len(nsroots) >= 1 {
rootedObjectAddr = naming.Join(nsroots[0], objectAddr)
} else {
rootedObjectAddr = s.rootedObjectAddrs[0].Name()
}
dispatcher := newDispatcher(macaroonKey, oauthBlesserParams(s.oauthBlesserParams, rootedObjectAddr))
if err := server.ServeDispatcher(objectAddr, dispatcher); err != nil {
return nil, nil, fmt.Errorf("failed to start Vanadium services: %v", err)
}
vlog.Infof("Blessing and discharger services will be published at %v", rootedObjectAddr)
return server, []string{rootedObjectAddr}, nil
}
// newDispatcher returns a dispatcher for both the blessing and the
// discharging service.
func newDispatcher(macaroonKey []byte, blesserParams blesser.OAuthBlesserParams) rpc.Dispatcher {
d := dispatcher(map[string]interface{}{
macaroonService: blesser.NewMacaroonBlesserServer(macaroonKey),
dischargerService: discharger.DischargerServer(dischargerlib.NewDischarger()),
oauthBlesserService: blesser.NewOAuthBlesserServer(blesserParams),
})
// Set up the glob invoker.
var children []string
for k, _ := range d {
children = append(children, k)
}
d[""] = rpc.ChildrenGlobberInvoker(children...)
return d
}
type dispatcher map[string]interface{}
func (d dispatcher) Lookup(suffix string) (interface{}, security.Authorizer, error) {
if invoker := d[suffix]; invoker != nil {
return invoker, security.AllowEveryone(), nil
}
return nil, nil, verror.New(verror.ErrNoExist, nil, suffix)
}
func oauthBlesserParams(inputParams blesser.OAuthBlesserParams, servername string) blesser.OAuthBlesserParams {
inputParams.DischargerLocation = naming.Join(servername, dischargerService)
return inputParams
}
func runHTTPSServer(addr, tlsConfig string) {
if len(tlsConfig) == 0 {
vlog.Fatal("Please set the --tls-config flag")
}
paths := strings.Split(tlsConfig, ",")
if len(paths) != 2 {
vlog.Fatalf("Could not parse --tls-config. Must have exactly two components, separated by a comma")
}
vlog.Infof("Starting HTTP server with TLS using certificate [%s] and private key [%s] at https://%s", paths[0], paths[1], addr)
if err := http.ListenAndServeTLS(addr, paths[0], paths[1], nil); err != nil {
vlog.Fatalf("http.ListenAndServeTLS failed: %v", err)
}
}
func httpAddress(externalHttpAddr, httpAddr string) string {
// If an externalHttpAddr is provided use that.
if externalHttpAddr != "" {
httpAddr = externalHttpAddr
}
return fmt.Sprintf("https://%v", httpAddr)
}