diff --git a/Documentation/microsoft-connector.md b/Documentation/microsoft-connector.md
new file mode 100644
index 0000000000000000000000000000000000000000..11024a6d91e04782fb432597b5386c86f52351b1
--- /dev/null
+++ b/Documentation/microsoft-connector.md
@@ -0,0 +1,112 @@
+# 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.
+
+### Caveats
+
+`groups` claim in dex is only supported when `tenant` is specified in Microsoft
+connector config. In order for dex to be able to list groups on behalf of
+logged in user, an explicit organization administrator consent is required. To
+obtain the consent do the following:
+
+  - when registering dex application on https://apps.dev.microsoft.com add
+    an explicit `Directory.Read.All` permission to the list of __Delegated
+    Permissions__
+  - open the following link in your browser and log in under organization
+    administrator account:
+
+`https://login.microsoftonline.com/<tenant>/adminconsent?client_id=<dex client id>`
+
+## 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
+```
+
+### Groups
+
+When the `groups` claim is present in a request to dex __and__ `tenant` is
+configured, dex will query Microsoft API to obtain a list of groups the user is
+a member of. `onlySecurityGroups` configuration option restricts the list to
+include only security groups. By default all groups (security, Office 365,
+mailing lists) are included.
+
+It is possible to require a user to be a member of a particular group in order
+to be successfully authenticated in dex. For example, with the following
+configuration file only the users who are members of at least one of the listed
+groups will be able to successfully authenticate in dex:
+
+```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: myorg.onmicrosoft.com
+      groups:
+        - developers
+        - devops
+```
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..7ea672fa147ad98223365ccc5ac85c7c05b950f3
--- /dev/null
+++ b/connector/microsoft/microsoft.go
@@ -0,0 +1,453 @@
+// Package microsoft provides authentication strategies using Microsoft.
+package microsoft
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"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"
+	// Microsoft requires this scope to list groups the user is a member of
+	// and resolve their UUIDs to groups names.
+	scopeGroups = "directory.read.all"
+)
+
+// 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"`
+	OnlySecurityGroups bool     `json:"onlySecurityGroups"`
+	Groups             []string `json:"groups"`
+}
+
+// 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,
+		onlySecurityGroups: c.OnlySecurityGroups,
+		groups:             c.Groups,
+		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
+	onlySecurityGroups bool
+	groups             []string
+	logger             logrus.FieldLogger
+}
+
+func (c *microsoftConnector) isOrgTenant() bool {
+	return c.tenant != "common" && c.tenant != "consumers" && c.tenant != "organizations"
+}
+
+func (c *microsoftConnector) groupsRequired(groupScope bool) bool {
+	return (len(c.groups) > 0 || groupScope) && c.isOrgTenant()
+}
+
+func (c *microsoftConnector) oauth2Config(scopes connector.Scopes) *oauth2.Config {
+	microsoftScopes := []string{scopeUser}
+	if c.groupsRequired(scopes.Groups) {
+		microsoftScopes = append(microsoftScopes, scopeGroups)
+	}
+
+	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 c.groupsRequired(s.Groups) {
+		groups, err := c.getGroups(ctx, client, user.ID)
+		if err != nil {
+			return identity, fmt.Errorf("microsoft: get groups: %v", err)
+		}
+		identity.Groups = groups
+	}
+
+	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
+
+	if c.groupsRequired(s.Groups) {
+		groups, err := c.getGroups(ctx, client, user.ID)
+		if err != nil {
+			return identity, fmt.Errorf("microsoft: get groups: %v", err)
+		}
+		identity.Groups = groups
+	}
+
+	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 {
+		return u, newGraphError(resp.Body)
+	}
+
+	if err := json.NewDecoder(resp.Body).Decode(&u); err != nil {
+		return u, fmt.Errorf("JSON decode: %v", err)
+	}
+
+	return u, err
+}
+
+// https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/resources/group
+// displayName - The display name for the group. This property is required when
+//               a group is created and it cannot be cleared during updates.
+//               Supports $filter and $orderby.
+type group struct {
+	Name string `json:"displayName"`
+}
+
+func (c *microsoftConnector) getGroups(ctx context.Context, client *http.Client, userID string) (groups []string, err error) {
+	ids, err := c.getGroupIDs(ctx, client)
+	if err != nil {
+		return groups, err
+	}
+
+	groups, err = c.getGroupNames(ctx, client, ids)
+	if err != nil {
+		return
+	}
+
+	// ensure that the user is in at least one required group
+	isInGroups := false
+	if len(c.groups) > 0 {
+		gs := make(map[string]struct{})
+		for _, g := range c.groups {
+			gs[g] = struct{}{}
+		}
+
+		for _, g := range groups {
+			if _, ok := gs[g]; ok {
+				isInGroups = true
+				break
+			}
+		}
+	}
+	if len(c.groups) > 0 && !isInGroups {
+		return nil, fmt.Errorf("microsoft: user %v not in required groups", userID)
+	}
+
+	return
+}
+
+func (c *microsoftConnector) getGroupIDs(ctx context.Context, client *http.Client) (ids []string, err error) {
+	// https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/user_getmembergroups
+	in := &struct {
+		SecurityEnabledOnly bool `json:"securityEnabledOnly"`
+	}{c.onlySecurityGroups}
+	reqURL := apiURL + "/v1.0/me/getMemberGroups"
+	for {
+		var out []string
+		var next string
+
+		next, err = c.post(ctx, client, reqURL, in, &out)
+		if err != nil {
+			return ids, err
+		}
+
+		ids = append(ids, out...)
+		if next == "" {
+			return
+		}
+		reqURL = next
+	}
+}
+
+func (c *microsoftConnector) getGroupNames(ctx context.Context, client *http.Client, ids []string) (groups []string, err error) {
+	if len(ids) == 0 {
+		return
+	}
+
+	// https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/directoryobject_getbyids
+	in := &struct {
+		IDs   []string `json:"ids"`
+		Types []string `json:"types"`
+	}{ids, []string{"group"}}
+	reqURL := apiURL + "/v1.0/directoryObjects/getByIds"
+	for {
+		var out []group
+		var next string
+
+		next, err = c.post(ctx, client, reqURL, in, &out)
+		if err != nil {
+			return groups, err
+		}
+
+		for _, g := range out {
+			groups = append(groups, g.Name)
+		}
+		if next == "" {
+			return
+		}
+		reqURL = next
+	}
+}
+
+func (c *microsoftConnector) post(ctx context.Context, client *http.Client, reqURL string, in interface{}, out interface{}) (string, error) {
+	var payload bytes.Buffer
+
+	err := json.NewEncoder(&payload).Encode(in)
+	if err != nil {
+		return "", fmt.Errorf("microsoft: JSON encode: %v", err)
+	}
+
+	req, err := http.NewRequest("POST", reqURL, &payload)
+	if err != nil {
+		return "", fmt.Errorf("new req: %v", err)
+	}
+	req.Header.Set("Content-Type", "application/json")
+
+	resp, err := client.Do(req.WithContext(ctx))
+	if err != nil {
+		return "", fmt.Errorf("post URL %v", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return "", newGraphError(resp.Body)
+	}
+
+	var next string
+	if err = json.NewDecoder(resp.Body).Decode(&struct {
+		NextLink *string     `json:"@odata.nextLink"`
+		Value    interface{} `json:"value"`
+	}{&next, out}); err != nil {
+		return "", fmt.Errorf("JSON decode: %v", err)
+	}
+
+	return next, nil
+}
+
+type graphError struct {
+	Code    string `json:"code"`
+	Message string `json:"message"`
+}
+
+func (e *graphError) Error() string {
+	return e.Code + ": " + e.Message
+}
+
+func newGraphError(r io.Reader) error {
+	// https://developer.microsoft.com/en-us/graph/docs/concepts/errors
+	var ge graphError
+	if err := json.NewDecoder(r).Decode(&struct {
+		Error *graphError `json:"error"`
+	}{&ge}); err != nil {
+		return fmt.Errorf("JSON error decode: %v", err)
+	}
+	return &ge
+}
+
+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;