add microsoft auth
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ linters-settings:
|
||||
golint:
|
||||
min-confidence: 0
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
min-complexity: 20
|
||||
maligned:
|
||||
suggest-new: true
|
||||
goconst:
|
||||
|
||||
+30
-4
@@ -1,7 +1,7 @@
|
||||
# auth - authentication via oauth2, direct and email
|
||||
[](https://github.com/go-pkgz/auth/actions) [](https://coveralls.io/github/go-pkgz/auth?branch=master) [](https://pkg.go.dev/github.com/go-pkgz/auth?tab=doc)
|
||||
|
||||
This library provides "social login" with Github, Google, Facebook, Twitter and Yandex as well as custom auth providers and email verification.
|
||||
This library provides "social login" with Github, Google, Facebook, Microsoft, Twitter, Yandex and Battle.net as well as custom auth providers and email verification.
|
||||
|
||||
- Multiple oauth2 providers can be used at the same time
|
||||
- Special `dev` provider allows local testing and development
|
||||
@@ -78,6 +78,8 @@ func main() {
|
||||
- `middleware.Admin` - requires authenticated admin user
|
||||
- `middleware.Trace` - doesn't require authenticated user, but adds user info to request
|
||||
|
||||
Also, there is a special middleware `middleware.UpdateUser` for population and modifying UserInfo in every request. See "Customization" for more details.
|
||||
|
||||
## Details
|
||||
|
||||
Generally, adding support of `auth` includes a few relatively simple steps:
|
||||
@@ -249,10 +251,11 @@ In order to add a new oauth2 provider following input is required:
|
||||
There are several ways to adjust functionality of the library:
|
||||
|
||||
1. `SecretReader` - interface with a single method `Get(aud string) string` to return the secret used for JWT signing and verification
|
||||
2. `ClaimsUpdater` - interface with `Update(claims Claims) Claims` method. This is the primary way to alter a token at login time and add any attributes, set ip, email, admin status and so on.
|
||||
3. `Validator` - interface with `Validate(token string, claims Claims) bool` method. This is post-token hook and will be called on **each request** wrapped with `Auth` middleware. This will be the place for special logic to reject some tokens or users.
|
||||
1. `ClaimsUpdater` - interface with `Update(claims Claims) Claims` method. This is the primary way to alter a token at login time and add any attributes, set ip, email, admin status and so on.
|
||||
1. `Validator` - interface with `Validate(token string, claims Claims) bool` method. This is post-token hook and will be called on **each request** wrapped with `Auth` middleware. This will be the place for special logic to reject some tokens or users.
|
||||
1. `UserUpdater` - interface with `Update(claims token.User) token.User` method. This method will be called on **each request** wrapped with `UpdateUser` middleware. This will be the place for special logic modify User Info in request context. [Example of usage.]((https://github.com/go-pkgz/auth/blob/master/_example/main.go#L148))
|
||||
|
||||
All of the interfaces above have corresponding Func adapters - `SecretFunc`, `ClaimsUpdFunc` and `ValidatorFunc`.
|
||||
All of the interfaces above have corresponding Func adapters - `SecretFunc`, `ClaimsUpdFunc`, `ValidatorFunc` and `UserUpdFunc`.
|
||||
|
||||
### Implementing black list logic or some other filters
|
||||
|
||||
@@ -332,6 +335,15 @@ Authentication handled by external providers. You should setup oauth2 for all (o
|
||||
|
||||
_instructions for google oauth2 setup borrowed from [oauth2_proxy](https://github.com/bitly/oauth2_proxy)_
|
||||
|
||||
#### Microsoft Auth Provider
|
||||
|
||||
1 .Register a new application [using the Azure portal](https://docs.microsoft.com/en-us/graph/auth-register-app-v2).
|
||||
2. Under **"Authentication/Platform configurations/Web"** enter the correct url constructed as domain + `/auth/microsoft/callback`. i.e. `https://example.mysite.com/auth/microsoft/callback`
|
||||
3. In "Overview" take note of the **Application (client) ID**
|
||||
4. Choose the new project from the top right project dropdown (only if another project is selected)
|
||||
5. Select "Certificates & secrets" and click on "+ New Client Secret".
|
||||
|
||||
|
||||
#### GitHub Auth Provider
|
||||
|
||||
1. Create a new **"OAuth App"**: https://github.com/settings/developers
|
||||
@@ -361,6 +373,20 @@ _instructions for google oauth2 setup borrowed from [oauth2_proxy](https://githu
|
||||
|
||||
For more details refer to [Yandex OAuth](https://tech.yandex.com/oauth/doc/dg/concepts/about-docpage/) and [Yandex.Passport](https://tech.yandex.com/passport/doc/dg/index-docpage/) API documentation.
|
||||
|
||||
##### Battle.net Auth Provider
|
||||
|
||||
1. Log into Battle.net as a developer: https://develop.battle.net/nav/login-redirect
|
||||
1. Click "+ CREATE CLIENT" https://develop.battle.net/access/clients/create
|
||||
1. For "Client name", enter whatever you want
|
||||
1. For "Redirect URLs", one of the lines must be "http\[s\]://your_remark_installation:port//auth/battlenet/callback", e.g. https://localhost:8443/auth/battlenet/callback or https://remark.mysite.com/auth/battlenet/callback
|
||||
1. For "Service URL", enter the URL to your site or check "I do not have a service URL for this client." checkbox if you don't have any
|
||||
1. For "Intended use", describe the application you're developing
|
||||
1. Click "Save".
|
||||
1. You can see your client ID and client secret at https://develop.battle.net/access/clients by clicking the client you created
|
||||
|
||||
For more details refer to [Complete Guide of Battle.net OAuth API and Login Button](https://hakanu.net/oauth/2017/01/26/complete-guide-of-battle-net-oauth-api-and-login-button/) or [the official Battle.net OAuth2 guide](https://develop.battle.net/documentation/guides/using-oauth)
|
||||
|
||||
|
||||
#### Twitter Auth Provider
|
||||
1. Create a new twitter application https://developer.twitter.com/en/apps
|
||||
1. Fill **App name** and **Description** and **URL** of your site
|
||||
|
||||
+5
-1
@@ -1,4 +1,4 @@
|
||||
// Package auth provides "social login" with Github, Google, Facebook and Yandex as well as custom auth providers.
|
||||
// Package auth provides "social login" with Github, Google, Facebook, Microsoft, Yandex and Battle.net as well as custom auth providers.
|
||||
package auth
|
||||
|
||||
import (
|
||||
@@ -222,6 +222,10 @@ func (s *Service) AddProvider(name, cid, csecret string) {
|
||||
s.providers = append(s.providers, provider.NewService(provider.NewFacebook(p)))
|
||||
case "yandex":
|
||||
s.providers = append(s.providers, provider.NewService(provider.NewYandex(p)))
|
||||
case "battlenet":
|
||||
s.providers = append(s.providers, provider.NewService(provider.NewBattlenet(p)))
|
||||
case "microsoft":
|
||||
s.providers = append(s.providers, provider.NewService(provider.NewMicrosoft(p)))
|
||||
case "twitter":
|
||||
s.providers = append(s.providers, provider.NewService(provider.NewTwitter(p)))
|
||||
case "dev":
|
||||
|
||||
+25
-15
@@ -34,7 +34,7 @@ type Proxy struct {
|
||||
}
|
||||
|
||||
// Put stores retrieved avatar to avatar.Store. Gets image from user info. Returns proxied url
|
||||
func (p *Proxy) Put(u token.User) (avatarURL string, err error) {
|
||||
func (p *Proxy) Put(u token.User, client *http.Client) (avatarURL string, err error) {
|
||||
|
||||
// no picture for user, try to generate identicon avatar
|
||||
if u.Picture == "" {
|
||||
@@ -52,29 +52,18 @@ func (p *Proxy) Put(u token.User) (avatarURL string, err error) {
|
||||
return p.URL + p.RoutePath + "/" + avatarID, nil
|
||||
}
|
||||
|
||||
// load avatar from remote location
|
||||
client := http.Client{Timeout: 10 * time.Second}
|
||||
var resp *http.Response
|
||||
err = retry(5, time.Second, func() error {
|
||||
var e error
|
||||
resp, e = client.Get(u.Picture)
|
||||
return e
|
||||
})
|
||||
body, err := p.load(u.Picture, client)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to fetch avatar from the orig")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if e := resp.Body.Close(); e != nil {
|
||||
if e := body.Close(); e != nil {
|
||||
p.Logf("[WARN] can't close response body, %s", e)
|
||||
}
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", errors.Errorf("failed to get avatar from the orig, status %s", resp.Status)
|
||||
}
|
||||
|
||||
avatarID, err := p.Store.Put(u.ID, p.resize(resp.Body, p.ResizeLimit)) // put returns avatar base name, like 123456.image
|
||||
avatarID, err := p.Store.Put(u.ID, p.resize(body, p.ResizeLimit)) // put returns avatar base name, like 123456.image
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -83,6 +72,27 @@ func (p *Proxy) Put(u token.User) (avatarURL string, err error) {
|
||||
return p.URL + p.RoutePath + "/" + avatarID, nil
|
||||
}
|
||||
|
||||
// load avatar from remote url and return body. Caller has to close the reader
|
||||
func (p *Proxy) load(url string, client *http.Client) (rc io.ReadCloser, err error) {
|
||||
// load avatar from remote location
|
||||
var resp *http.Response
|
||||
err = retry(5, time.Second, func() error {
|
||||
var e error
|
||||
resp, e = client.Get(url)
|
||||
return e
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to fetch avatar from the orig")
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
_ = resp.Body.Close() // caller won't close on error
|
||||
return nil, errors.Errorf("failed to get avatar from the orig, status %s", resp.Status)
|
||||
}
|
||||
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
// Handler returns token routes for given provider
|
||||
func (p *Proxy) Handler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
|
||||
+2
-2
@@ -1,5 +1,7 @@
|
||||
module github.com/go-pkgz/auth
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/dghubble/oauth1 v0.6.0
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
@@ -14,5 +16,3 @@ require (
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
gopkg.in/oauth2.v3 v3.12.0
|
||||
)
|
||||
|
||||
go 1.13
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-pkgz/auth/token"
|
||||
)
|
||||
|
||||
// UserUpdater defines interface adding extras or modifying UserInfo in request context
|
||||
type UserUpdater interface {
|
||||
Update(claims token.User) token.User
|
||||
}
|
||||
|
||||
// UserUpdFunc type is an adapter to allow the use of ordinary functions as UserUpdater. If f is a function
|
||||
// with the appropriate signature, UserUpdFunc(f) is a Handler that calls f.
|
||||
type UserUpdFunc func(user token.User) token.User
|
||||
|
||||
// Update calls f(user)
|
||||
func (f UserUpdFunc) Update(user token.User) token.User {
|
||||
return f(user)
|
||||
}
|
||||
|
||||
// UpdateUser update user info with UserUpdater if it exists in request's context. Otherwise do nothing.
|
||||
// should be places after either Auth, Trace or AdminOnly middleware.
|
||||
func (a *Authenticator) UpdateUser(upd UserUpdater) func(http.Handler) http.Handler {
|
||||
f := func(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
// call update only if user info exists, otherwise do nothing
|
||||
if user, err := token.GetUserInfo(r); err == nil {
|
||||
r = token.SetUserInfo(r, upd.Update(user))
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
return f
|
||||
}
|
||||
+2
-1
@@ -4,6 +4,7 @@ import (
|
||||
"crypto/sha1"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/go-pkgz/rest"
|
||||
@@ -63,7 +64,7 @@ func (p DirectHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
Name: user,
|
||||
ID: p.ProviderName + "_" + token.HashID(sha1.New(), user),
|
||||
}
|
||||
u, err = setAvatar(p.AvatarSaver, u)
|
||||
u, err = setAvatar(p.AvatarSaver, u, &http.Client{Timeout: 5 * time.Second})
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, p.L, http.StatusInternalServerError, err, "failed to save avatar to proxy")
|
||||
return
|
||||
|
||||
+1
-1
@@ -127,7 +127,7 @@ func (h Oauth1Handler) AuthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
h.Logf("[DEBUG] got raw user info %+v", jData)
|
||||
|
||||
u := h.mapUser(jData, data)
|
||||
u, err = setAvatar(h.AvatarSaver, u)
|
||||
u, err = setAvatar(h.AvatarSaver, u, &http.Client{Timeout: 5 * time.Second})
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, h.L, http.StatusInternalServerError, err, "failed to save avatar to proxy")
|
||||
return
|
||||
|
||||
+1
-1
@@ -178,7 +178,7 @@ func (p Oauth2Handler) AuthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
p.Logf("[DEBUG] got raw user info %+v", jData)
|
||||
|
||||
u := p.mapUser(jData, data)
|
||||
u, err = setAvatar(p.AvatarSaver, u)
|
||||
u, err = setAvatar(p.AvatarSaver, u, client)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, p.L, http.StatusInternalServerError, err, "failed to save avatar to proxy")
|
||||
return
|
||||
|
||||
+44
@@ -6,9 +6,11 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/facebook"
|
||||
"golang.org/x/oauth2/github"
|
||||
"golang.org/x/oauth2/google"
|
||||
"golang.org/x/oauth2/microsoft"
|
||||
"golang.org/x/oauth2/yandex"
|
||||
|
||||
"github.com/dghubble/oauth1"
|
||||
@@ -147,3 +149,45 @@ func NewTwitter(p Params) Oauth1Handler {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// NewBattlenet makes Battle.net oauth2 provider
|
||||
func NewBattlenet(p Params) Oauth2Handler {
|
||||
return initOauth2Handler(p, Oauth2Handler{
|
||||
name: "battlenet",
|
||||
endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://eu.battle.net/oauth/authorize",
|
||||
TokenURL: "https://eu.battle.net/oauth/token",
|
||||
AuthStyle: oauth2.AuthStyleInParams,
|
||||
},
|
||||
scopes: []string{},
|
||||
infoURL: "https://eu.battle.net/oauth/userinfo",
|
||||
mapUser: func(data UserData, _ []byte) token.User {
|
||||
userInfo := token.User{
|
||||
ID: "battlenet_" + token.HashID(sha1.New(), data.Value("id")),
|
||||
Name: data.Value("battletag"),
|
||||
}
|
||||
|
||||
return userInfo
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// NewMicrosoft makes microsoft azure oauth2 provider
|
||||
func NewMicrosoft(p Params) Oauth2Handler {
|
||||
return initOauth2Handler(p, Oauth2Handler{
|
||||
name: "microsoft",
|
||||
endpoint: microsoft.AzureADEndpoint("consumers"),
|
||||
scopes: []string{"User.Read"},
|
||||
infoURL: "https://graph.microsoft.com/v1.0/me",
|
||||
// non-beta doesn't provide photo for consumers yet
|
||||
// see https://github.com/microsoftgraph/microsoft-graph-docs/issues/3990
|
||||
mapUser: func(data UserData, b []byte) token.User {
|
||||
userInfo := token.User{
|
||||
ID: "microsoft_" + token.HashID(sha1.New(), data.Value("id")),
|
||||
Name: data.Value("displayName"),
|
||||
Picture: "https://graph.microsoft.com/beta/me/photo/$value",
|
||||
}
|
||||
return userInfo
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
+3
-3
@@ -30,7 +30,7 @@ func NewService(p Provider) Service {
|
||||
|
||||
// AvatarSaver defines minimal interface to save avatar
|
||||
type AvatarSaver interface {
|
||||
Put(u token.User) (avatarURL string, err error)
|
||||
Put(u token.User, client *http.Client) (avatarURL string, err error)
|
||||
}
|
||||
|
||||
// TokenService defines interface accessing tokens
|
||||
@@ -72,9 +72,9 @@ func (p Service) Handler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// setAvatar saves avatar and puts proxied URL to u.Picture
|
||||
func setAvatar(ava AvatarSaver, u token.User) (token.User, error) {
|
||||
func setAvatar(ava AvatarSaver, u token.User, client *http.Client) (token.User, error) {
|
||||
if ava != nil {
|
||||
avatarURL, e := ava.Put(u)
|
||||
avatarURL, e := ava.Put(u, client)
|
||||
if e != nil {
|
||||
return u, errors.Wrap(e, "failed to save avatar for")
|
||||
}
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ func (e VerifyHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if u, err = setAvatar(e.AvatarSaver, u); err != nil {
|
||||
if u, err = setAvatar(e.AvatarSaver, u, &http.Client{Timeout: 5 * time.Second}); err != nil {
|
||||
rest.SendErrorJSON(w, r, e.L, http.StatusInternalServerError, err, "failed to save avatar to proxy")
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user