diff --git a/Documentation/microsoft-connector.md b/Documentation/microsoft-connector.md
new file mode 100644
index 0000000000000000000000000000000000000000..a7318ba0ce825f069dfc4db883f18b1dd651f33b
--- /dev/null
+++ b/Documentation/microsoft-connector.md
@@ -0,0 +1,66 @@
+# Authentication through Microsoft
+
+## Overview
+
+One of the login options for dex uses the Microsoft OAuth2 flow to identify the
+end user through their Microsoft account.
+
+When a client redeems a refresh token through dex, dex will re-query Microsoft
+to update user information in the ID Token. To do this, __dex stores a readonly
+Microsoft access and refresh tokens in its backing datastore.__ Users that
+reject dex's access through Microsoft will also revoke all dex clients which
+authenticated them through Microsoft.
+
+## Configuration
+
+Register a new application on https://apps.dev.microsoft.com via `Add an app`
+ensuring the callback URL is `(dex issuer)/callback`. For example if dex
+is listening at the non-root path `https://auth.example.com/dex` the callback
+would be `https://auth.example.com/dex/callback`.
+
+The following is an example of a configuration for `examples/config-dev.yaml`:
+
+```yaml
+connectors:
+  - type: microsoft
+    # Required field for connector id.
+    id: microsoft
+    # Required field for connector name.
+    name: Microsoft
+    config:
+      # Credentials can be string literals or pulled from the environment.
+      clientID: $MICROSOFT_APPLICATION_ID
+      clientSecret: $MICROSOFT_CLIENT_SECRET
+      redirectURI: http://127.0.0.1:5556/dex/callback
+```
+
+`tenant` configuration parameter controls what kinds of accounts may be
+authenticated in dex. By default, all types of Microsoft accounts (consumers
+and organizations) can authenticate in dex via Microsoft. To change this, set
+the `tenant` parameter to one of the following:
+
+- `common`- both personal and business/school accounts can authenticate in dex
+  via Microsoft (default)
+- `consumers` - only personal accounts can authenticate in dex
+- `organizations` - only business/school accounts can authenticate in dex
+- `<tenant uuid>` or `<tenant name>` - only accounts belonging to specific
+  tenant identified by either `<tenant uuid>` or `<tenant name>` can
+  authenticate in dex
+
+For example, the following snippet configures dex to only allow business/school
+accounts:
+
+```yaml
+connectors:
+  - type: microsoft
+    # Required field for connector id.
+    id: microsoft
+    # Required field for connector name.
+    name: Microsoft
+    config:
+      # Credentials can be string literals or pulled from the environment.
+      clientID: $MICROSOFT_APPLICATION_ID
+      clientSecret: $MICROSOFT_CLIENT_SECRET
+      redirectURI: http://127.0.0.1:5556/dex/callback
+      tenant: organizations
+```
diff --git a/README.md b/README.md
index d536eff040d8c789daa401c4deca536e5f7db048..1352c642b1d6be55e9a9c2fa1f974a68109ef0f7 100644
--- a/README.md
+++ b/README.md
@@ -69,6 +69,7 @@ More docs for running dex as a Kubernetes authenticator can be found [here](Docu
   * [OpenID Connect](Documentation/oidc-connector.md) (includes Google, Salesforce, Azure, etc.)
   * [authproxy](Documentation/authproxy.md) (Apache2 mod_auth, etc.)
   * [LinkedIn](Documentation/linkedin-connector.md)
+  * [Microsoft](Documentation/microsoft-connection.md)
 * Client libraries
   * [Go][go-oidc]
 
diff --git a/connector/microsoft/microsoft.go b/connector/microsoft/microsoft.go
new file mode 100644
index 0000000000000000000000000000000000000000..4f2b49a81dea939102d0ee25335953866abfd9c7
--- /dev/null
+++ b/connector/microsoft/microsoft.go
@@ -0,0 +1,282 @@
+// Package microsoft provides authentication strategies using Microsoft.
+package microsoft
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"sync"
+	"time"
+
+	"golang.org/x/oauth2"
+
+	"github.com/coreos/dex/connector"
+	"github.com/sirupsen/logrus"
+)
+
+const (
+	apiURL = "https://graph.microsoft.com"
+	// Microsoft requires this scope to access user's profile
+	scopeUser = "user.read"
+)
+
+// Config holds configuration options for microsoft logins.
+type Config struct {
+	ClientID     string `json:"clientID"`
+	ClientSecret string `json:"clientSecret"`
+	RedirectURI  string `json:"redirectURI"`
+	Tenant       string `json:"tenant"`
+}
+
+// Open returns a strategy for logging in through Microsoft.
+func (c *Config) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) {
+	m := microsoftConnector{
+		redirectURI:  c.RedirectURI,
+		clientID:     c.ClientID,
+		clientSecret: c.ClientSecret,
+		tenant:       c.Tenant,
+		logger:       logger,
+	}
+	// By default allow logins from both personal and business/school
+	// accounts.
+	if m.tenant == "" {
+		m.tenant = "common"
+	}
+
+	return &m, nil
+}
+
+type connectorData struct {
+	AccessToken  string    `json:"accessToken"`
+	RefreshToken string    `json:"refreshToken"`
+	Expiry       time.Time `json:"expiry"`
+}
+
+var (
+	_ connector.CallbackConnector = (*microsoftConnector)(nil)
+	_ connector.RefreshConnector  = (*microsoftConnector)(nil)
+)
+
+type microsoftConnector struct {
+	redirectURI  string
+	clientID     string
+	clientSecret string
+	tenant       string
+	logger       logrus.FieldLogger
+}
+
+func (c *microsoftConnector) oauth2Config(scopes connector.Scopes) *oauth2.Config {
+	microsoftScopes := []string{scopeUser}
+
+	return &oauth2.Config{
+		ClientID:     c.clientID,
+		ClientSecret: c.clientSecret,
+		Endpoint: oauth2.Endpoint{
+			AuthURL:  "https://login.microsoftonline.com/" + c.tenant + "/oauth2/v2.0/authorize",
+			TokenURL: "https://login.microsoftonline.com/" + c.tenant + "/oauth2/v2.0/token",
+		},
+		Scopes:      microsoftScopes,
+		RedirectURL: c.redirectURI,
+	}
+}
+
+func (c *microsoftConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, error) {
+	if c.redirectURI != callbackURL {
+		return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI)
+	}
+
+	return c.oauth2Config(scopes).AuthCodeURL(state), nil
+}
+
+func (c *microsoftConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) {
+	q := r.URL.Query()
+	if errType := q.Get("error"); errType != "" {
+		return identity, &oauth2Error{errType, q.Get("error_description")}
+	}
+
+	oauth2Config := c.oauth2Config(s)
+
+	ctx := r.Context()
+
+	token, err := oauth2Config.Exchange(ctx, q.Get("code"))
+	if err != nil {
+		return identity, fmt.Errorf("microsoft: failed to get token: %v", err)
+	}
+
+	client := oauth2Config.Client(ctx, token)
+
+	user, err := c.user(ctx, client)
+	if err != nil {
+		return identity, fmt.Errorf("microsoft: get user: %v", err)
+	}
+
+	identity = connector.Identity{
+		UserID:        user.ID,
+		Username:      user.Name,
+		Email:         user.Email,
+		EmailVerified: true,
+	}
+
+	if s.OfflineAccess {
+		data := connectorData{
+			AccessToken:  token.AccessToken,
+			RefreshToken: token.RefreshToken,
+			Expiry:       token.Expiry,
+		}
+		connData, err := json.Marshal(data)
+		if err != nil {
+			return identity, fmt.Errorf("microsoft: marshal connector data: %v", err)
+		}
+		identity.ConnectorData = connData
+	}
+
+	return identity, nil
+}
+
+type tokenNotifyFunc func(*oauth2.Token) error
+
+// notifyRefreshTokenSource is essentially `oauth2.ResuseTokenSource` with `TokenNotifyFunc` added.
+type notifyRefreshTokenSource struct {
+	new oauth2.TokenSource
+	mu  sync.Mutex // guards t
+	t   *oauth2.Token
+	f   tokenNotifyFunc // called when token refreshed so new refresh token can be persisted
+}
+
+// Token returns the current token if it's still valid, else will
+// refresh the current token (using r.Context for HTTP client
+// information) and return the new one.
+func (s *notifyRefreshTokenSource) Token() (*oauth2.Token, error) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if s.t.Valid() {
+		return s.t, nil
+	}
+	t, err := s.new.Token()
+	if err != nil {
+		return nil, err
+	}
+	s.t = t
+	return t, s.f(t)
+}
+
+func (c *microsoftConnector) Refresh(ctx context.Context, s connector.Scopes, identity connector.Identity) (connector.Identity, error) {
+	if len(identity.ConnectorData) == 0 {
+		return identity, errors.New("microsoft: no upstream access token found")
+	}
+
+	var data connectorData
+	if err := json.Unmarshal(identity.ConnectorData, &data); err != nil {
+		return identity, fmt.Errorf("microsoft: unmarshal access token: %v", err)
+	}
+	tok := &oauth2.Token{
+		AccessToken:  data.AccessToken,
+		RefreshToken: data.RefreshToken,
+		Expiry:       data.Expiry,
+	}
+
+	client := oauth2.NewClient(ctx, &notifyRefreshTokenSource{
+		new: c.oauth2Config(s).TokenSource(ctx, tok),
+		t:   tok,
+		f: func(tok *oauth2.Token) error {
+			data := connectorData{
+				AccessToken:  tok.AccessToken,
+				RefreshToken: tok.RefreshToken,
+				Expiry:       tok.Expiry,
+			}
+			connData, err := json.Marshal(data)
+			if err != nil {
+				return fmt.Errorf("microsoft: marshal connector data: %v", err)
+			}
+			identity.ConnectorData = connData
+			return nil
+		},
+	})
+	user, err := c.user(ctx, client)
+	if err != nil {
+		return identity, fmt.Errorf("microsoft: get user: %v", err)
+	}
+
+	identity.Username = user.Name
+	identity.Email = user.Email
+
+	return identity, nil
+}
+
+// https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/resources/user
+// id                - The unique identifier for the user. Inherited from
+//                     directoryObject. Key. Not nullable. Read-only.
+// displayName       - The name displayed in the address book for the user.
+//                     This is usually the combination of the user's first name,
+//                     middle initial and last name. This property is required
+//                     when a user is created and it cannot be cleared during
+//                     updates. Supports $filter and $orderby.
+// userPrincipalName - The user principal name (UPN) of the user.
+//                     The UPN is an Internet-style login name for the user
+//                     based on the Internet standard RFC 822. By convention,
+//                     this should map to the user's email name. The general
+//                     format is alias@domain, where domain must be present in
+//                     the tenant’s collection of verified domains. This
+//                     property is required when a user is created. The
+//                     verified domains for the tenant can be accessed from the
+//                     verifiedDomains property of organization. Supports
+//                     $filter and $orderby.
+type user struct {
+	ID    string `json:"id"`
+	Name  string `json:"displayName"`
+	Email string `json:"userPrincipalName"`
+}
+
+func (c *microsoftConnector) user(ctx context.Context, client *http.Client) (u user, err error) {
+	// https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/user_get
+	req, err := http.NewRequest("GET", apiURL+"/v1.0/me?$select=id,displayName,userPrincipalName", nil)
+	if err != nil {
+		return u, fmt.Errorf("new req: %v", err)
+	}
+
+	resp, err := client.Do(req.WithContext(ctx))
+	if err != nil {
+		return u, fmt.Errorf("get URL %v", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		// https://developer.microsoft.com/en-us/graph/docs/concepts/errors
+		var ge graphError
+		if err := json.NewDecoder(resp.Body).Decode(&struct {
+			Error *graphError `json:"error"`
+		}{&ge}); err != nil {
+			return u, fmt.Errorf("JSON error decode: %v", err)
+		}
+		return u, &ge
+	}
+
+	if err := json.NewDecoder(resp.Body).Decode(&u); err != nil {
+		return u, fmt.Errorf("JSON decode: %v", err)
+	}
+
+	return u, err
+}
+
+type graphError struct {
+	Code    string `json:"code"`
+	Message string `json:"message"`
+}
+
+func (e *graphError) Error() string {
+	return e.Code + ": " + e.Message
+}
+
+type oauth2Error struct {
+	error            string
+	errorDescription string
+}
+
+func (e *oauth2Error) Error() string {
+	if e.errorDescription == "" {
+		return e.error
+	}
+	return e.error + ": " + e.errorDescription
+}
diff --git a/server/server.go b/server/server.go
index f915b5acb66eaad7646e7be6dd92400149d3167f..6b609cc772ba219f3c5b301a5371249af6a2e102 100644
--- a/server/server.go
+++ b/server/server.go
@@ -25,6 +25,7 @@ import (
 	"github.com/coreos/dex/connector/gitlab"
 	"github.com/coreos/dex/connector/ldap"
 	"github.com/coreos/dex/connector/linkedin"
+	"github.com/coreos/dex/connector/microsoft"
 	"github.com/coreos/dex/connector/mock"
 	"github.com/coreos/dex/connector/oidc"
 	"github.com/coreos/dex/connector/saml"
@@ -415,6 +416,7 @@ var ConnectorsConfig = map[string]func() ConnectorConfig{
 	"saml":         func() ConnectorConfig { return new(saml.Config) },
 	"authproxy":    func() ConnectorConfig { return new(authproxy.Config) },
 	"linkedin":     func() ConnectorConfig { return new(linkedin.Config) },
+	"microsoft":    func() ConnectorConfig { return new(microsoft.Config) },
 	// Keep around for backwards compatibility.
 	"samlExperimental": func() ConnectorConfig { return new(saml.Config) },
 }
diff --git a/web/static/img/microsoft-icon.svg b/web/static/img/microsoft-icon.svg
new file mode 100644
index 0000000000000000000000000000000000000000..739c395abe8656bbc5cd317bd3bcae533d679916
--- /dev/null
+++ b/web/static/img/microsoft-icon.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" height="439" width="439">
+<rect height="439" width="439" fill="#f3f3f3"/>
+<rect height="194" width="194" x="17"  y="17"  fill="#F35325"/>
+<rect height="194" width="194" x="228" y="17"  fill="#81BC06"/>
+<rect height="194" width="194" x="17"  y="228" fill="#05A6F0"/>
+<rect height="194" width="194" x="228" y="228" fill="#FFBA08"/>
+</svg>
\ No newline at end of file
diff --git a/web/static/main.css b/web/static/main.css
index c147f35c403fc2998f4a1d11cb8d0376c8fc3a8b..552479adfa602ce890fd63a61d695716f6005f88 100644
--- a/web/static/main.css
+++ b/web/static/main.css
@@ -88,6 +88,10 @@ body {
   background-size: contain;
 }
 
+.dex-btn-icon--microsoft {
+  background-image: url(../static/img/microsoft-icon.svg);
+}
+
 .dex-btn-text {
   font-weight: 600;
   line-height: 36px;