Auth Provider (#1309)
* auth provider mock interface * Auth Provider Options * Implement API Server Auth Package * Add weh utils * Add Login URL * Auth Provider Options * Add auth provider scope and setting token in cookie * Remove auth_login_url flag Co-authored-by: Asim Aslam <asim@aslam.me> Co-authored-by: Ben Toogood <ben@micro.mu>
This commit is contained in:
parent
8ee5607254
commit
9a7a65f05e
70
api/server/auth/auth.go
Normal file
70
api/server/auth/auth.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/micro/go-micro/v2/auth"
|
||||||
|
"github.com/micro/go-micro/v2/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CombinedAuthHandler wraps a server and authenticates requests
|
||||||
|
func CombinedAuthHandler(h http.Handler) http.Handler {
|
||||||
|
return authHandler{
|
||||||
|
handler: h,
|
||||||
|
auth: auth.DefaultAuth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type authHandler struct {
|
||||||
|
handler http.Handler
|
||||||
|
auth auth.Auth
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// BearerScheme is the prefix in the auth header
|
||||||
|
BearerScheme = "Bearer "
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h authHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
loginURL := h.auth.Options().LoginURL
|
||||||
|
|
||||||
|
// Return if the user disabled auth on this endpoint
|
||||||
|
excludes := h.auth.Options().Exclude
|
||||||
|
if len(loginURL) > 0 {
|
||||||
|
excludes = append(excludes, loginURL)
|
||||||
|
}
|
||||||
|
for _, e := range excludes {
|
||||||
|
if e == req.URL.Path {
|
||||||
|
h.handler.ServeHTTP(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var token string
|
||||||
|
if header, ok := metadata.Get(req.Context(), "Authorization"); ok {
|
||||||
|
// Extract the auth token from the request
|
||||||
|
if strings.HasPrefix(header, BearerScheme) {
|
||||||
|
token = header[len(BearerScheme):]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Get the token out the cookies if not provided in headers
|
||||||
|
if c, err := req.Cookie(auth.CookieName); err != nil && c != nil {
|
||||||
|
token = c.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the token is valid, allow the request
|
||||||
|
if _, err := h.auth.Verify(token); err == nil {
|
||||||
|
h.handler.ServeHTTP(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is no auth login url set, 401
|
||||||
|
if loginURL == "" {
|
||||||
|
w.WriteHeader(401)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to the login path
|
||||||
|
http.Redirect(w, req, loginURL, http.StatusTemporaryRedirect)
|
||||||
|
}
|
@ -8,6 +8,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/micro/go-micro/v2/api/server/auth"
|
||||||
|
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/micro/go-micro/v2/api/server"
|
"github.com/micro/go-micro/v2/api/server"
|
||||||
"github.com/micro/go-micro/v2/api/server/cors"
|
"github.com/micro/go-micro/v2/api/server/cors"
|
||||||
@ -47,6 +49,7 @@ func (s *httpServer) Init(opts ...server.Option) error {
|
|||||||
|
|
||||||
func (s *httpServer) Handle(path string, handler http.Handler) {
|
func (s *httpServer) Handle(path string, handler http.Handler) {
|
||||||
h := handlers.CombinedLoggingHandler(os.Stdout, handler)
|
h := handlers.CombinedLoggingHandler(os.Stdout, handler)
|
||||||
|
h = auth.CombinedAuthHandler(handler)
|
||||||
|
|
||||||
if s.opts.EnableCORS {
|
if s.opts.EnableCORS {
|
||||||
h = cors.CombinedCORSHandler(h)
|
h = cors.CombinedCORSHandler(h)
|
||||||
|
@ -44,7 +44,7 @@ type Role struct {
|
|||||||
|
|
||||||
// Account provided by an auth provider
|
// Account provided by an auth provider
|
||||||
type Account struct {
|
type Account struct {
|
||||||
// ID of the account (UUID or email)
|
// ID of the account (UUIDV4, email or username)
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
// Token used to authenticate
|
// Token used to authenticate
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
@ -62,6 +62,9 @@ const (
|
|||||||
// MetadataKey is the key used when storing the account
|
// MetadataKey is the key used when storing the account
|
||||||
// in metadata
|
// in metadata
|
||||||
MetadataKey = "auth-account"
|
MetadataKey = "auth-account"
|
||||||
|
// CookieName is the name of the cookie which stores the
|
||||||
|
// auth token
|
||||||
|
CookieName = "micro-token"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AccountFromContext gets the account from the context, which
|
// AccountFromContext gets the account from the context, which
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
|
import "github.com/micro/go-micro/v2/auth/provider"
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
// Token is an auth token
|
// Token is an auth token
|
||||||
Token string
|
Token string
|
||||||
@ -9,6 +11,10 @@ type Options struct {
|
|||||||
PrivateKey string
|
PrivateKey string
|
||||||
// Endpoints to exclude
|
// Endpoints to exclude
|
||||||
Exclude []string
|
Exclude []string
|
||||||
|
// Provider is an auth provider
|
||||||
|
Provider provider.Provider
|
||||||
|
// LoginURL is the relative url path where a user can login
|
||||||
|
LoginURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Option func(o *Options)
|
type Option func(o *Options)
|
||||||
@ -41,6 +47,20 @@ func Token(t string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Provider set the auth provider
|
||||||
|
func Provider(p provider.Provider) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Provider = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginURL sets the auth LoginURL
|
||||||
|
func LoginURL(url string) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.LoginURL = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type GenerateOptions struct {
|
type GenerateOptions struct {
|
||||||
// Metadata associated with the account
|
// Metadata associated with the account
|
||||||
Metadata map[string]string
|
Metadata map[string]string
|
||||||
|
34
auth/provider/basic/basic.go
Normal file
34
auth/provider/basic/basic.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package basic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/micro/go-micro/v2/auth/provider"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewProvider returns an initialised basic provider
|
||||||
|
func NewProvider(opts ...provider.Option) provider.Provider {
|
||||||
|
var options provider.Options
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&options)
|
||||||
|
}
|
||||||
|
return &basic{options}
|
||||||
|
}
|
||||||
|
|
||||||
|
type basic struct {
|
||||||
|
opts provider.Options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *basic) String() string {
|
||||||
|
return "basic"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *basic) Options() provider.Options {
|
||||||
|
return b.opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *basic) Endpoint() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *basic) Redirect() string {
|
||||||
|
return ""
|
||||||
|
}
|
42
auth/provider/oauth/oauth.go
Normal file
42
auth/provider/oauth/oauth.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package oauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/micro/go-micro/v2/auth/provider"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewProvider returns an initialised oauth provider
|
||||||
|
func NewProvider(opts ...provider.Option) provider.Provider {
|
||||||
|
var options provider.Options
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&options)
|
||||||
|
}
|
||||||
|
return &oauth{options}
|
||||||
|
}
|
||||||
|
|
||||||
|
type oauth struct {
|
||||||
|
opts provider.Options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *oauth) String() string {
|
||||||
|
return "oauth"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *oauth) Options() provider.Options {
|
||||||
|
return o.opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *oauth) Endpoint() string {
|
||||||
|
s := fmt.Sprintf("%v?client_id=%v", o.opts.Endpoint, o.opts.ClientID)
|
||||||
|
|
||||||
|
if scope := o.opts.Scope; len(scope) > 0 {
|
||||||
|
s = fmt.Sprintf("%v&scope=%v", s, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *oauth) Redirect() string {
|
||||||
|
return o.opts.Redirect
|
||||||
|
}
|
47
auth/provider/options.go
Normal file
47
auth/provider/options.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
// Option returns a function which sets an option
|
||||||
|
type Option func(*Options)
|
||||||
|
|
||||||
|
// Options a provider can have
|
||||||
|
type Options struct {
|
||||||
|
// ClientID is the application's ID.
|
||||||
|
ClientID string
|
||||||
|
// ClientSecret is the application's secret.
|
||||||
|
ClientSecret string
|
||||||
|
// Endpoint for the provider
|
||||||
|
Endpoint string
|
||||||
|
// Redirect url incase of UI
|
||||||
|
Redirect string
|
||||||
|
// Scope of the oauth request
|
||||||
|
Scope string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Credentials is an option which sets the client id and secret
|
||||||
|
func Credentials(id, secret string) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.ClientID = id
|
||||||
|
o.ClientSecret = secret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Endpoint sets the endpoint option
|
||||||
|
func Endpoint(e string) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Endpoint = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect sets the Redirect option
|
||||||
|
func Redirect(r string) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Redirect = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scope sets the oauth scope
|
||||||
|
func Scope(s string) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Scope = s
|
||||||
|
}
|
||||||
|
}
|
28
auth/provider/provider.go
Normal file
28
auth/provider/provider.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// Package provider is an external auth provider e.g oauth
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Provider is an auth provider
|
||||||
|
type Provider interface {
|
||||||
|
// String returns the name of the provider
|
||||||
|
String() string
|
||||||
|
// Options returns the options of a provider
|
||||||
|
Options() Options
|
||||||
|
// Endpoint for the provider
|
||||||
|
Endpoint() string
|
||||||
|
// Redirect url incase of UI
|
||||||
|
Redirect() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grant is a granted authorisation
|
||||||
|
type Grant struct {
|
||||||
|
// token for reuse
|
||||||
|
Token string
|
||||||
|
// Expiry of the token
|
||||||
|
Expiry time.Time
|
||||||
|
// Scopes associated with grant
|
||||||
|
Scopes []string
|
||||||
|
}
|
@ -7,6 +7,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/micro/go-micro/v2/auth/provider"
|
||||||
|
|
||||||
"github.com/micro/go-micro/v2/auth"
|
"github.com/micro/go-micro/v2/auth"
|
||||||
"github.com/micro/go-micro/v2/broker"
|
"github.com/micro/go-micro/v2/broker"
|
||||||
"github.com/micro/go-micro/v2/client"
|
"github.com/micro/go-micro/v2/client"
|
||||||
@ -70,6 +72,10 @@ import (
|
|||||||
jwtAuth "github.com/micro/go-micro/v2/auth/jwt"
|
jwtAuth "github.com/micro/go-micro/v2/auth/jwt"
|
||||||
sAuth "github.com/micro/go-micro/v2/auth/service"
|
sAuth "github.com/micro/go-micro/v2/auth/service"
|
||||||
storeAuth "github.com/micro/go-micro/v2/auth/store"
|
storeAuth "github.com/micro/go-micro/v2/auth/store"
|
||||||
|
|
||||||
|
// auth providers
|
||||||
|
"github.com/micro/go-micro/v2/auth/provider/basic"
|
||||||
|
"github.com/micro/go-micro/v2/auth/provider/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cmd interface {
|
type Cmd interface {
|
||||||
@ -269,6 +275,36 @@ var (
|
|||||||
EnvVars: []string{"MICRO_AUTH_EXCLUDE"},
|
EnvVars: []string{"MICRO_AUTH_EXCLUDE"},
|
||||||
Usage: "Comma-separated list of endpoints excluded from authentication, e.g. Users.ListUsers",
|
Usage: "Comma-separated list of endpoints excluded from authentication, e.g. Users.ListUsers",
|
||||||
},
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "auth_provider",
|
||||||
|
EnvVars: []string{"MICRO_AUTH_PROVIDER"},
|
||||||
|
Usage: "Auth provider used to login user",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "auth_provider_client_id",
|
||||||
|
EnvVars: []string{"MICRO_AUTH_PROVIDER_CLIENT_ID"},
|
||||||
|
Usage: "The client id to be used for oauth",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "auth_provider_client_secret",
|
||||||
|
EnvVars: []string{"MICRO_AUTH_PROVIDER_CLIENT_SECRET"},
|
||||||
|
Usage: "The client secret to be used for oauth",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "auth_provider_endpoint",
|
||||||
|
EnvVars: []string{"MICRO_AUTH_PROVIDER_ENDPOINT"},
|
||||||
|
Usage: "The enpoint to be used for oauth",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "auth_provider_redirect",
|
||||||
|
EnvVars: []string{"MICRO_AUTH_PROVIDER_REDIRECT"},
|
||||||
|
Usage: "The redirect to be used for oauth",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "auth_provider_scope",
|
||||||
|
EnvVars: []string{"MICRO_AUTH_PROVIDER_SCOPE"},
|
||||||
|
Usage: "The scope to be used for oauth",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
DefaultBrokers = map[string]func(...broker.Option) broker.Broker{
|
DefaultBrokers = map[string]func(...broker.Option) broker.Broker{
|
||||||
@ -328,6 +364,11 @@ var (
|
|||||||
"jwt": jwtAuth.NewAuth,
|
"jwt": jwtAuth.NewAuth,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DefaultAuthProviders = map[string]func(...provider.Option) provider.Provider{
|
||||||
|
"oauth": oauth.NewProvider,
|
||||||
|
"basic": basic.NewProvider,
|
||||||
|
}
|
||||||
|
|
||||||
DefaultProfiles = map[string]func(...profile.Option) profile.Profile{
|
DefaultProfiles = map[string]func(...profile.Option) profile.Profile{
|
||||||
"http": http.NewProfile,
|
"http": http.NewProfile,
|
||||||
"pprof": pprof.NewProfile,
|
"pprof": pprof.NewProfile,
|
||||||
@ -627,6 +668,32 @@ func (c *cmd) Before(ctx *cli.Context) error {
|
|||||||
authOpts = append(authOpts, auth.Exclude(ctx.StringSlice("auth_exclude")...))
|
authOpts = append(authOpts, auth.Exclude(ctx.StringSlice("auth_exclude")...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if name := ctx.String("auth_provider"); len(name) > 0 {
|
||||||
|
p, ok := DefaultAuthProviders[name]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("AuthProvider %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
var provOpts []provider.Option
|
||||||
|
|
||||||
|
clientID := ctx.String("auth_provider_client_id")
|
||||||
|
clientSecret := ctx.String("auth_provider_client_secret")
|
||||||
|
if len(clientID) > 0 || len(clientSecret) > 0 {
|
||||||
|
provOpts = append(provOpts, provider.Credentials(clientID, clientSecret))
|
||||||
|
}
|
||||||
|
if e := ctx.String("auth_provider_endpoint"); len(e) > 0 {
|
||||||
|
provOpts = append(provOpts, provider.Endpoint(e))
|
||||||
|
}
|
||||||
|
if r := ctx.String("auth_provider_redirect"); len(r) > 0 {
|
||||||
|
provOpts = append(provOpts, provider.Redirect(r))
|
||||||
|
}
|
||||||
|
if s := ctx.String("auth_provider_scope"); len(s) > 0 {
|
||||||
|
provOpts = append(provOpts, provider.Scope(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
authOpts = append(authOpts, auth.Provider(p(provOpts...)))
|
||||||
|
}
|
||||||
|
|
||||||
if len(authOpts) > 0 {
|
if len(authOpts) > 0 {
|
||||||
if err := (*c.opts.Auth).Init(authOpts...); err != nil {
|
if err := (*c.opts.Auth).Init(authOpts...); err != nil {
|
||||||
log.Fatalf("Error configuring auth: %v", err)
|
log.Fatalf("Error configuring auth: %v", err)
|
||||||
|
@ -1,12 +1,47 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/micro/go-micro/v2/client/selector"
|
"github.com/micro/go-micro/v2/client/selector"
|
||||||
"github.com/micro/go-micro/v2/registry"
|
"github.com/micro/go-micro/v2/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Write sets the status and body on a http ResponseWriter
|
||||||
|
func Write(w http.ResponseWriter, contentType string, status int, body string) {
|
||||||
|
w.Header().Set("Content-Length", fmt.Sprintf("%v", len(body)))
|
||||||
|
w.Header().Set("Content-Type", contentType)
|
||||||
|
w.WriteHeader(status)
|
||||||
|
fmt.Fprintf(w, `%v`, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteBadRequestError sets a 400 status code
|
||||||
|
func WriteBadRequestError(w http.ResponseWriter, err error) {
|
||||||
|
rawBody, err := json.Marshal(map[string]string{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
WriteInternalServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Write(w, "application/json", 400, string(rawBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteInternalServerError sets a 500 status code
|
||||||
|
func WriteInternalServerError(w http.ResponseWriter, err error) {
|
||||||
|
rawBody, err := json.Marshal(map[string]string{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Write(w, "application/json", 500, string(rawBody))
|
||||||
|
}
|
||||||
|
|
||||||
func NewRoundTripper(opts ...Option) http.RoundTripper {
|
func NewRoundTripper(opts ...Option) http.RoundTripper {
|
||||||
options := Options{
|
options := Options{
|
||||||
Registry: registry.DefaultRegistry,
|
Registry: registry.DefaultRegistry,
|
||||||
|
Loading…
Reference in New Issue
Block a user