add microsoft auth

This commit is contained in:
Umputun
2020-06-18 18:25:35 -05:00
parent 42d0c67cb3
commit df9c05b490
18 changed files with 205 additions and 35 deletions
+11 -1
View File
@@ -4,7 +4,7 @@
Remark42 is a self-hosted, lightweight, and simple (yet functional) comment engine, which doesn't spy on users. It can be embedded into blogs, articles or any other place where readers add comments.
* Social login via Google, Twitter, Facebook, GitHub and Yandex
* Social login via Google, Twitter, Facebook, Microsoft, GitHub and Yandex
* Login via email
* Optional anonymous access
* Multi-level nested comments with both tree and plain presentations
@@ -135,6 +135,8 @@ _this is the recommended way to run remark42_
| auth.google.csec | AUTH_GOOGLE_CSEC | | Google OAuth client secret |
| auth.facebook.cid | AUTH_FACEBOOK_CID | | Facebook OAuth client ID |
| auth.facebook.csec | AUTH_FACEBOOK_CSEC | | Facebook OAuth client secret |
| auth.microsoft.cid | AUTH_MICROSOFT_CID | | Microsoft OAuth client ID |
| auth.microsoft.csec | AUTH_MICROSOFT_CSEC | | Microsoft OAuth client secret |
| auth.github.cid | AUTH_GITHUB_CID | | Github OAuth client ID |
| auth.github.csec | AUTH_GITHUB_CSEC | | Github OAuth client secret |
| auth.twitter.cid | AUTH_TWITTER_CID | | Twitter Consumer API Key |
@@ -284,6 +286,14 @@ _instructions for google oauth2 setup borrowed from [oauth2_proxy](https://githu
1. Under **"Facebook login"** / **"Settings"** fill "Valid OAuth redirect URIs" with your callback url constructed as domain + `/auth/facebook/callback`
1. Select **"App Review"** and turn public flag on. This step may ask you to provide a link to your privacy policy.
#### 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".
##### Twitter Auth Provider
1. Create a new twitter application https://developer.twitter.com/en/apps
+5
View File
@@ -83,6 +83,7 @@ type ServerCommand struct {
Google AuthGroup `group:"google" namespace:"google" env-namespace:"GOOGLE" description:"Google OAuth"`
Github AuthGroup `group:"github" namespace:"github" env-namespace:"GITHUB" description:"Github OAuth"`
Facebook AuthGroup `group:"facebook" namespace:"facebook" env-namespace:"FACEBOOK" description:"Facebook OAuth"`
Microsoft AuthGroup `group:"microsoft" namespace:"microsoft" env-namespace:"MICROSOFT" description:"Microsoft OAuth"`
Yandex AuthGroup `group:"yandex" namespace:"yandex" env-namespace:"YANDEX" description:"Yandex OAuth"`
Twitter AuthGroup `group:"twitter" namespace:"twitter" env-namespace:"TWITTER" description:"Twitter OAuth"`
Dev bool `long:"dev" env:"DEV" description:"enable dev (local) oauth2"`
@@ -697,6 +698,10 @@ func (s *ServerCommand) addAuthProviders(authenticator *auth.Service) error {
authenticator.AddProvider("facebook", s.Auth.Facebook.CID, s.Auth.Facebook.CSEC)
providers++
}
if s.Auth.Microsoft.CID != "" && s.Auth.Microsoft.CSEC != "" {
authenticator.AddProvider("microsoft", s.Auth.Microsoft.CID, s.Auth.Microsoft.CSEC)
providers++
}
if s.Auth.Yandex.CID != "" && s.Auth.Yandex.CSEC != "" {
authenticator.AddProvider("yandex", s.Auth.Yandex.CID, s.Auth.Yandex.CSEC)
providers++
+1 -1
View File
@@ -12,7 +12,7 @@ require (
github.com/go-chi/chi v4.1.1+incompatible
github.com/go-chi/cors v1.1.1
github.com/go-chi/render v1.0.1
github.com/go-pkgz/auth v0.10.2
github.com/go-pkgz/auth v0.11.0
github.com/go-pkgz/jrpc v0.2.0
github.com/go-pkgz/lcw v0.6.1
github.com/go-pkgz/lgr v0.7.0
+2 -2
View File
@@ -56,8 +56,8 @@ github.com/go-chi/cors v1.1.1 h1:eHuqxsIw89iXcWnWUN8R72JMibABJTN/4IOYI5WERvw=
github.com/go-chi/cors v1.1.1/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/go-pkgz/auth v0.10.2 h1:aYTEu0sxBi+UJXm3IIn9/cgW7bTXZkiykY23rdYZjAc=
github.com/go-pkgz/auth v0.10.2/go.mod h1:w4Z1qaYvuh2P3T2gNh0f8GcKCH0HHAoQtQ8iv+9+WGg=
github.com/go-pkgz/auth v0.11.0 h1:xi9Y0KGZUftVLx/8tGdvIfUk/+/4QLBVL6k9A8C4ycM=
github.com/go-pkgz/auth v0.11.0/go.mod h1:NzVqlTW0E9JXVdAaWRq81XZjICgHnNaNdUfE3CbS2T4=
github.com/go-pkgz/expirable-cache v0.0.3 h1:rTh6qNPp78z0bQE6HDhXBHUwqnV9i09Vm6dksJLXQDc=
github.com/go-pkgz/expirable-cache v0.0.3/go.mod h1:+IauqN00R2FqNRLCLA+X5YljQJrwB179PfiAoMPlTlQ=
github.com/go-pkgz/jrpc v0.2.0 h1:CLy/eZyekjraVrxZV18N2R1mYLMJ/nWrgdfyIOGPY/E=
+1 -1
View File
@@ -4,7 +4,7 @@ linters-settings:
golint:
min-confidence: 0
gocyclo:
min-complexity: 15
min-complexity: 20
maligned:
suggest-new: true
goconst:
+30 -4
View File
@@ -1,7 +1,7 @@
# auth - authentication via oauth2, direct and email
[![Build Status](https://github.com/go-pkgz/auth/workflows/build/badge.svg)](https://github.com/go-pkgz/auth/actions) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/auth/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/auth?branch=master) [![godoc](https://godoc.org/github.com/go-pkgz/auth?status.svg)](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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+31
View File
@@ -0,0 +1,31 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package microsoft provides constants for using OAuth2 to access Windows Live ID.
package microsoft // import "golang.org/x/oauth2/microsoft"
import (
"golang.org/x/oauth2"
)
// LiveConnectEndpoint is Windows's Live ID OAuth 2.0 endpoint.
var LiveConnectEndpoint = oauth2.Endpoint{
AuthURL: "https://login.live.com/oauth20_authorize.srf",
TokenURL: "https://login.live.com/oauth20_token.srf",
}
// AzureADEndpoint returns a new oauth2.Endpoint for the given tenant at Azure Active Directory.
// If tenant is empty, it uses the tenant called `common`.
//
// For more information see:
// https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints
func AzureADEndpoint(tenant string) oauth2.Endpoint {
if tenant == "" {
tenant = "common"
}
return oauth2.Endpoint{
AuthURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/authorize",
TokenURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/token",
}
}
+2 -1
View File
@@ -72,7 +72,7 @@ github.com/go-chi/cors
# github.com/go-chi/render v1.0.1
## explicit
github.com/go-chi/render
# github.com/go-pkgz/auth v0.10.2
# github.com/go-pkgz/auth v0.11.0
## explicit
github.com/go-pkgz/auth
github.com/go-pkgz/auth/avatar
@@ -243,6 +243,7 @@ golang.org/x/oauth2/google
golang.org/x/oauth2/internal
golang.org/x/oauth2/jws
golang.org/x/oauth2/jwt
golang.org/x/oauth2/microsoft
golang.org/x/oauth2/yandex
# golang.org/x/sync v0.0.0-20190423024810-112230192c58
golang.org/x/sync/semaphore