diff --git a/README.md b/README.md
index bcf084547e933657f05596a2f341cf59a393c88d..9507f3ad8a7fca1cf2480009896829b67eb77a25 100644
--- a/README.md
+++ b/README.md
@@ -178,11 +178,13 @@ Supported options:
 
 | Provider |       Option | Notes                                                                                                                                  |
 |---------:|-------------:|----------------------------------------------------------------------------------------------------------------------------------------|
-| Google   |       domain | If this is unset then you must whitelist individual email addresses using `users_whitelist`.                                           |
 | Github   | organization | If this is unset then you must whitelist individual users using `users_whitelist`. The oauth client and secrets should be issued by the specified organization. |
-| Gitlab   | siteurl | Optional. The url of the Gitlab site. Default: `https://gitlab.com/api/v3/` |
 | Gitlab   | allusers | Allow all valid users to get signed keys. Only allowed if siteurl set. |
 | Gitlab   | group | If `allusers` and this are unset then you must whitelist individual users using `users_whitelist`. Otherwise the user must be a member of this group. |
+| Gitlab   | siteurl | Optional. The url of the Gitlab site. Default: `https://gitlab.com/api/v3/` |
+| Google   | domain | If this is unset then you must whitelist individual email addresses using `users_whitelist`. |
+| Microsoft | groups | Comma separated list of valid groups. |
+| Microsoft | tenant | The domain name of the Office 365 account. |
 
 ## ssh
 - `signing_key`: string. Path to the certificate signing ssh private key. Use `ssh-keygen` to create the key and store it somewhere safe. See also the [note](#a-note-on-files) on files above.
diff --git a/cmd/cashier/main.go b/cmd/cashier/main.go
index f448a2520467fcf0ef7d503616d7db2d1e09f877..1ee945592643429a2e464719ad2d6f0b1e3c6283 100644
--- a/cmd/cashier/main.go
+++ b/cmd/cashier/main.go
@@ -1,6 +1,9 @@
 package main
 
 import (
+	"bufio"
+	"bytes"
+	"encoding/base64"
 	"fmt"
 	"log"
 	"net"
@@ -46,8 +49,16 @@ func main() {
 	}
 
 	fmt.Print("Enter token: ")
-	var token string
-	fmt.Scanln(&token)
+	scanner := bufio.NewScanner(os.Stdin)
+	var buffer bytes.Buffer
+	for scanner.Scan(); scanner.Text() != "."; scanner.Scan() {
+		buffer.WriteString(scanner.Text())
+	}
+	tokenBytes, err := base64.StdEncoding.DecodeString(buffer.String())
+	if err != nil {
+		log.Fatalln(err)
+	}
+	token := string(tokenBytes)
 
 	cert, err := client.Sign(pub, token, c)
 	if err != nil {
diff --git a/server/auth/microsoft/microsoft.go b/server/auth/microsoft/microsoft.go
new file mode 100644
index 0000000000000000000000000000000000000000..49d9b822b66a26e9a140a16b8ed91254568718eb
--- /dev/null
+++ b/server/auth/microsoft/microsoft.go
@@ -0,0 +1,203 @@
+package microsoft
+
+import (
+	"encoding/json"
+	"errors"
+	"net/http"
+	"path"
+	"strings"
+
+	"github.com/nsheridan/cashier/server/auth"
+	"github.com/nsheridan/cashier/server/config"
+	"github.com/nsheridan/cashier/server/metrics"
+
+	"golang.org/x/oauth2"
+	"golang.org/x/oauth2/microsoft"
+)
+
+const (
+	name = "microsoft"
+)
+
+// Config is an implementation of `auth.Provider` for authenticating using a
+// Office 365 account.
+type Config struct {
+	config    *oauth2.Config
+	tenant    string
+	groups    map[string]bool
+	whitelist map[string]bool
+}
+
+var _ auth.Provider = (*Config)(nil)
+
+// New creates a new Microsoft provider from a configuration.
+func New(c *config.Auth) (*Config, error) {
+	whitelist := make(map[string]bool)
+	for _, u := range c.UsersWhitelist {
+		whitelist[u] = true
+	}
+	if c.ProviderOpts["tenant"] == "" && len(whitelist) == 0 {
+		return nil, errors.New("either Office 365 tenant or users whitelist must be specified")
+	}
+	groupMap := make(map[string]bool)
+	if groups, ok := c.ProviderOpts["groups"]; ok {
+		for _, group := range strings.Split(groups, ",") {
+			groupMap[strings.Trim(group, " ")] = true
+		}
+	}
+
+	return &Config{
+		config: &oauth2.Config{
+			ClientID:     c.OauthClientID,
+			ClientSecret: c.OauthClientSecret,
+			RedirectURL:  c.OauthCallbackURL,
+			Endpoint:     microsoft.AzureADEndpoint(c.ProviderOpts["tenant"]),
+			Scopes:       []string{"user.Read.All", "Directory.Read.All"},
+		},
+		tenant:    c.ProviderOpts["tenant"],
+		whitelist: whitelist,
+		groups:    groupMap,
+	}, nil
+}
+
+// A new oauth2 http client.
+func (c *Config) newClient(token *oauth2.Token) *http.Client {
+	return c.config.Client(oauth2.NoContext, token)
+}
+
+// Gets a response for an graph api call.
+func (c *Config) getDocument(token *oauth2.Token, pathElements ...string) map[string]interface{} {
+	client := c.newClient(token)
+	url := "https://" + path.Join("graph.microsoft.com/v1.0", path.Join(pathElements...))
+	resp, err := client.Get(url)
+	if err != nil {
+		return nil
+	}
+	defer resp.Body.Close()
+	var document map[string]interface{}
+	if err := json.NewDecoder(resp.Body).Decode(&document); err != nil {
+		return nil
+	}
+	return document
+}
+
+// Get info from the "/me" endpoint of the Microsoft Graph API (MSG-API).
+// https://developer.microsoft.com/en-us/graph/docs/concepts/v1-overview
+func (c *Config) getMe(token *oauth2.Token, item string) string {
+	document := c.getDocument(token, "/me")
+	if value, ok := document[item].(string); ok {
+		return value
+	}
+	return ""
+}
+
+// Check against verified domains from "/organization" endpoint of MSG-API.
+func (c *Config) verifyTenant(token *oauth2.Token) bool {
+	document := c.getDocument(token, "/organization")
+	// The domains for an organisation are in an array of structs under
+	// verifiedDomains, which is in a struct which is in turn an array
+	// of such structs under value in the document.  Which in json looks
+	// like this:
+	// { "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#organization",
+	//   "value": [ {
+	//      ...
+	//      "verifiedDomains": [ {
+	//                    ...
+	//                    "name": "M365x214355.onmicrosoft.com",
+	//            } ]
+	//   } ]
+	//}
+	var value []interface{}
+	var ok bool
+	if value, ok = document["value"].([]interface{}); !ok {
+		return false
+	}
+	for _, valueEntry := range value {
+		if value, ok = valueEntry.(map[string]interface{})["verifiedDomains"].([]interface{}); !ok {
+			continue
+		}
+		for _, val := range value {
+			domain := val.(map[string]interface{})["name"].(string)
+			if domain == c.tenant {
+				return true
+			}
+		}
+	}
+	return false
+}
+
+// Check against groups from /users/{id}/memberOf endpoint of MSG-API.
+func (c *Config) verifyGroups(token *oauth2.Token) bool {
+	document := c.getDocument(token, "/users/me/memberOf")
+	var value []interface{}
+	var ok bool
+	if value, ok = document["value"].([]interface{}); !ok {
+		return false
+	}
+	for _, valueEntry := range value {
+		if group, ok := valueEntry.(map[string]interface{})["displayName"].(string); ok {
+			if c.groups[group] {
+				return true
+			}
+		}
+	}
+	return false
+}
+
+// Name returns the name of the provider.
+func (c *Config) Name() string {
+	return name
+}
+
+// Valid validates the oauth token.
+func (c *Config) Valid(token *oauth2.Token) bool {
+	if len(c.whitelist) > 0 && !c.whitelist[c.Email(token)] {
+		return false
+	}
+	if !token.Valid() {
+		return false
+	}
+	metrics.M.AuthValid.WithLabelValues("microsoft").Inc()
+	if c.tenant != "" {
+		if c.verifyTenant(token) {
+			if len(c.groups) > 0 {
+				return c.verifyGroups(token)
+			}
+			return true
+		}
+	}
+	return false
+}
+
+// Revoke disables the access token.
+func (c *Config) Revoke(token *oauth2.Token) error {
+	return nil
+}
+
+// StartSession retrieves an authentication endpoint from Microsoft.
+func (c *Config) StartSession(state string) *auth.Session {
+	return &auth.Session{
+		AuthURL: c.config.AuthCodeURL(state,
+			oauth2.SetAuthURLParam("hd", c.tenant),
+			oauth2.SetAuthURLParam("prompt", "login")),
+	}
+}
+
+// Exchange authorizes the session and returns an access token.
+func (c *Config) Exchange(code string) (*oauth2.Token, error) {
+	t, err := c.config.Exchange(oauth2.NoContext, code)
+	if err == nil {
+		metrics.M.AuthExchange.WithLabelValues("microsoft").Inc()
+	}
+	return t, err
+}
+
+// Email retrieves the email address of the user.
+func (c *Config) Email(token *oauth2.Token) string {
+	return c.getMe(token, "mail")
+}
+
+// Username retrieves the username portion of the user's email address.
+func (c *Config) Username(token *oauth2.Token) string {
+	return strings.Split(c.Email(token), "@")[0]
+}
diff --git a/server/auth/microsoft/microsoft_test.go b/server/auth/microsoft/microsoft_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..c2c2c1722d9c011beb789e1ba003249bd6fc1fcb
--- /dev/null
+++ b/server/auth/microsoft/microsoft_test.go
@@ -0,0 +1,72 @@
+package microsoft
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/nsheridan/cashier/server/config"
+	"github.com/stretchr/testify/assert"
+)
+
+var (
+	oauthClientID     = "id"
+	oauthClientSecret = "secret"
+	oauthCallbackURL  = "url"
+	tenant            = "example.com"
+	users             = []string{"user"}
+)
+
+func TestNew(t *testing.T) {
+	a := assert.New(t)
+	p, err := newMicrosoft()
+	a.NoError(err)
+	a.Equal(p.config.ClientID, oauthClientID)
+	a.Equal(p.config.ClientSecret, oauthClientSecret)
+	a.Equal(p.config.RedirectURL, oauthCallbackURL)
+	a.Equal(p.tenant, tenant)
+	a.Equal(p.whitelist, map[string]bool{"user": true})
+}
+
+func TestWhitelist(t *testing.T) {
+	c := &config.Auth{
+		OauthClientID:     oauthClientID,
+		OauthClientSecret: oauthClientSecret,
+		OauthCallbackURL:  oauthCallbackURL,
+		ProviderOpts:      map[string]string{"tenant": ""},
+		UsersWhitelist:    []string{},
+	}
+	if _, err := New(c); err == nil {
+		t.Error("creating a provider without a tenant set should return an error")
+	}
+	// Set a user whitelist but no tenant
+	c.UsersWhitelist = users
+	if _, err := New(c); err != nil {
+		t.Error("creating a provider with users but no tenant should not return an error")
+	}
+	// Unset the user whitelist and set a tenant
+	c.UsersWhitelist = []string{}
+	c.ProviderOpts = map[string]string{"tenant": tenant}
+	if _, err := New(c); err != nil {
+		t.Error("creating a provider with a tenant set but without a user whitelist should not return an error")
+	}
+}
+
+func TestStartSession(t *testing.T) {
+	a := assert.New(t)
+
+	p, err := newMicrosoft()
+	a.NoError(err)
+	s := p.StartSession("test_state")
+	a.Contains(s.AuthURL, fmt.Sprintf("login.microsoftonline.com/%s/oauth2/v2.0/authorize", tenant))
+}
+
+func newMicrosoft() (*Config, error) {
+	c := &config.Auth{
+		OauthClientID:     oauthClientID,
+		OauthClientSecret: oauthClientSecret,
+		OauthCallbackURL:  oauthCallbackURL,
+		ProviderOpts:      map[string]string{"tenant": tenant},
+		UsersWhitelist:    users,
+	}
+	return New(c)
+}
diff --git a/server/server.go b/server/server.go
index 42476f3895f2e90634a06dd48d0f90a2e3f35aa8..1b8468e48b7fb008f13f867464dd669bcad84003 100644
--- a/server/server.go
+++ b/server/server.go
@@ -16,6 +16,7 @@ import (
 	"github.com/nsheridan/cashier/server/auth/github"
 	"github.com/nsheridan/cashier/server/auth/gitlab"
 	"github.com/nsheridan/cashier/server/auth/google"
+	"github.com/nsheridan/cashier/server/auth/microsoft"
 	"github.com/nsheridan/cashier/server/config"
 	"github.com/nsheridan/cashier/server/metrics"
 	"github.com/nsheridan/cashier/server/signer"
@@ -90,12 +91,14 @@ func Run(conf *config.Config) {
 	metrics.Register()
 
 	switch conf.Auth.Provider {
-	case "google":
-		authprovider, err = google.New(conf.Auth)
 	case "github":
 		authprovider, err = github.New(conf.Auth)
 	case "gitlab":
 		authprovider, err = gitlab.New(conf.Auth)
+	case "google":
+		authprovider, err = google.New(conf.Auth)
+	case "microsoft":
+		authprovider, err = microsoft.New(conf.Auth)
 	default:
 		log.Fatalf("Unknown provider %s\n", conf.Auth.Provider)
 	}
diff --git a/server/web.go b/server/web.go
index e238150cfe455f090d927d12ee13de81a88ce397..d55aa523a34aa02910110734e430fcc158de57fb 100644
--- a/server/web.go
+++ b/server/web.go
@@ -1,7 +1,9 @@
 package server
 
 import (
+	"bytes"
 	"crypto/rand"
+	"encoding/base64"
 	"encoding/hex"
 	"encoding/json"
 	"fmt"
@@ -189,6 +191,23 @@ func callbackHandler(a *appContext, w http.ResponseWriter, r *http.Request) (int
 	return http.StatusFound, nil
 }
 
+func encodeString(s string) string {
+	var buffer bytes.Buffer
+	chunkSize := 70
+	runes := []rune(base64.StdEncoding.EncodeToString([]byte(s)))
+
+	for i := 0; i < len(runes); i += chunkSize {
+		end := i + chunkSize
+		if end > len(runes) {
+			end = len(runes)
+		}
+		buffer.WriteString(string(runes[i:end]))
+		buffer.WriteString("\n")
+	}
+	buffer.WriteString(".\n")
+	return buffer.String()
+}
+
 // rootHandler starts the auth process. If the client is authenticated it renders the token to the user.
 func rootHandler(a *appContext, w http.ResponseWriter, r *http.Request) (int, error) {
 	if !a.isLoggedIn(w, r) {
@@ -198,6 +217,7 @@ func rootHandler(a *appContext, w http.ResponseWriter, r *http.Request) (int, er
 	page := struct {
 		Token string
 	}{tok.AccessToken}
+	page.Token = encodeString(page.Token)
 
 	tmpl := template.Must(template.New("token.html").Parse(templates.Token))
 	tmpl.Execute(w, page)
diff --git a/vendor/golang.org/x/oauth2/microsoft/microsoft.go b/vendor/golang.org/x/oauth2/microsoft/microsoft.go
new file mode 100644
index 0000000000000000000000000000000000000000..3ffbc57a6906ca8e8fa9d49960e97e45efd5df54
--- /dev/null
+++ b/vendor/golang.org/x/oauth2/microsoft/microsoft.go
@@ -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",
+	}
+}
diff --git a/vendor/vendor.json b/vendor/vendor.json
index aca060d5e7cca6041269b628bb2a44342fa0322c..1b1f6cf2361d202440a403177bb343c579531540 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -978,6 +978,12 @@
 			"revision": "ef147856a6ddbb60760db74283d2424e98c87bff",
 			"revisionTime": "2018-06-20T17:47:24Z"
 		},
+		{
+			"checksumSHA1": "91mzAbqHQ6AAK65DzB4IkLOcvtk=",
+			"path": "golang.org/x/oauth2/microsoft",
+			"revision": "ef147856a6ddbb60760db74283d2424e98c87bff",
+			"revisionTime": "2018-06-20T17:47:24Z"
+		},
 		{
 			"checksumSHA1": "S0DP7Pn7sZUmXc55IzZnNvERu6s=",
 			"path": "golang.org/x/sync/errgroup",