diff --git a/README.md b/README.md
index c671e8990da4d1b4bc268985f33f21a45eff36d7..1fb9b6f954fad11bc3751137af949cd8cf450a0c 100644
--- a/README.md
+++ b/README.md
@@ -63,6 +63,7 @@ Almost all OAuth2/OIDC providers will require you to register a redirect URI. Us
 ## Provider specific Tips
 
  * [Google](/docs/GOOGLE.md)
+ * [LinkedIn](/docs/LINKEDIN.md)
  * [Microsoft](/docs/MICROSOFT.md)
 
 ## Generic Usage
diff --git a/attributemap/linkedin2name.php b/attributemap/linkedin2name.php
new file mode 100644
index 0000000000000000000000000000000000000000..6c3556eb6a961f8a55ff46a441a8aa7ddb131985
--- /dev/null
+++ b/attributemap/linkedin2name.php
@@ -0,0 +1,9 @@
+<?php
+$attributemap = array(
+
+    'linkedin.firstName' => 'givenName',
+    'linkedin.lastName' => 'sn',
+    'linkedin.id' => 'uid', // any b64 character
+    'linkedin.emailAddress' => 'mail',
+
+);
diff --git a/docs/GOOGLE.md b/docs/GOOGLE.md
index 03fa401177a1ad3961c8776b0537cfcb3c6bebce..3982a77989004cf423e4e619494595c896ca4607 100644
--- a/docs/GOOGLE.md
+++ b/docs/GOOGLE.md
@@ -12,12 +12,13 @@
 
 # Google as an AuthSource
 
-Google provides both OIDC and Google Plus endpoints for learning about
-a user.  The OIDC endpoints require fewer client API permissions and
-return data in a standardized format. The Google Plus endpoints can
-return more data about a user but require Google Plus permissions and
-return data in a Google specific format. The Google Plus apis will be shutting down sometime in 2019
-so we recommend using the OIDC endpoints
+Google provides OIDC (and previously Google Plus endpoints for
+learning about a user).  The OIDC endpoints require fewer client API
+permissions and return data in a standardized format. The Google Plus
+endpoints can return more data about a user but require Google Plus
+permissions and return data in a Google specific format. The Google
+Plus apis will be shutting down sometime in 2019 so we recommend using
+the OIDC endpoints
 
 You can also choose between using the generic OAuth/OIDC implementation or using
 a [Google specific library](https://github.com/thephpleague/oauth2-google/).
@@ -25,7 +26,7 @@ a [Google specific library](https://github.com/thephpleague/oauth2-google/).
 # Usage
 ## Recommended Config
 
-We recommend using the OIDC configuration with the generic implementation. This
+We recommend using the OIDC configuration with the generic OAuth2 authsource. This
 requires the least configuration.
 
 
@@ -59,6 +60,7 @@ If you want to restrict the hosted domain of a user you can pass the
 returned from Google matches what you expect - a user could remove the
 `hd` from the browser flow and login with any account.
 
+* Out of date *
 TODO: Once https://github.com/thephpleague/oauth2-google/pull/54 is accepted into the oauth2-google project then
 this check would be done automatically. This example would then need to be updated to use that project
 
diff --git a/docs/LINKEDIN.md b/docs/LINKEDIN.md
new file mode 100644
index 0000000000000000000000000000000000000000..9f25f04adfd5b10405c41d28a98da2b48941512c
--- /dev/null
+++ b/docs/LINKEDIN.md
@@ -0,0 +1,50 @@
+<!-- START doctoc generated TOC please keep comment here to allow auto update -->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents**  *generated with [DocToc](https://github.com/thlorenz/doctoc)*
+
+- [LinkedIn as authsource](#linkedin-as-authsource)
+- [Usage](#usage)
+- [Migrarting from OAuth v1 authlinkedin](#migrarting-from-oauth-v1-authlinkedin)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+# LinkedIn as authsource
+
+LinkedIn recommends using OAuth2 and their v2 apis. Their v1 apis and
+OAuth1 endpoints are being shutdown. LinkedIn v2 apis return data in a
+more complex format (json keys change based on language) and require
+additional API calls to get an email address. You need to use the
+`authoauth2:LinkedInV2Auth` authsource since LinkedIn doesn't conform
+the expected OIDC/OAuth pattern.
+
+# Usage
+
+```php
+   'linkedin' => [
+        'authoauth2:LinkedInV2Auth',
+        'clientId' => $apiKey,
+        'clientSecret' =>  $apiSecret,
+        // Adjust the scopes: default is to request email and liteprofile
+        // 'scopes' => ['r_liteprofile'], 
+    ],
+```
+
+# Migrarting from OAuth v1 authlinkedin
+
+The `authlinkedin` module bundled with most version of SSP uses
+deprecated OAuth v1 and v1 API endpoints.  To migrate to the new
+LinkedIn api you will need to add a [redirect URI to your linkedin
+application](https://docs.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow?context=linkedin/consumer/context). The
+redirect URI is
+
+    https://hostname/SSP_PATH/module.php/authoauth2/linkback.php
+
+You will then need to change your `authsource` configuration to match the example usage above.
+
+On your idp side you may need to use `linkedin2name` from this module rather than `authlinkedin`.
+
+```php
+        // Convert linkedin names to ldap friendly names
+        10 => array('class' => 'core:AttributeMap',  'authoauth2:linkedin2name'),
+```
+There are some minor changes in user experience and consent which are outlined in [our blog post](https://blog.cirrusidentity.com/linkedin-user-interaction-changes).
\ No newline at end of file
diff --git a/lib/Auth/Source/LinkedInV2Auth.php b/lib/Auth/Source/LinkedInV2Auth.php
new file mode 100644
index 0000000000000000000000000000000000000000..aa00afd6aa0e2f439b1b81e89f1698ece545e037
--- /dev/null
+++ b/lib/Auth/Source/LinkedInV2Auth.php
@@ -0,0 +1,138 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: patrick
+ * Date: 10/16/18
+ * Time: 1:34 PM
+ */
+
+namespace SimpleSAML\Module\authoauth2\Auth\Source;
+
+use League\OAuth2\Client\Provider\AbstractProvider;
+use League\OAuth2\Client\Token\AccessToken;
+use SimpleSAML\Logger;
+use SimpleSAML\Module\authoauth2\ConfigTemplate;
+
+/**
+ * LinkedIn's v2 api requires a 2nd call to determine email address.
+ */
+class LinkedInV2Auth extends OAuth2
+{
+
+    public function __construct(array $info, array $config)
+    {
+        // Set some defaults
+        if (!array_key_exists('template', $config)) {
+            $config['template'] = 'LinkedInV2';
+        }
+        parent::__construct($info, $config);
+    }
+
+    public function convertResourceOwnerAttributes(array $resourceOwnerAttributes, $prefix)
+    {
+        /** Sample response from LinkedIn
+         * {
+         * "lastName":{
+         * "localized":{
+         * "en_US":"Lasty"
+         * },
+         * "preferredLocale":{
+         * "country":"US",
+         * "language":"en"
+         * }
+         * },
+         * "firstName":{
+         * "localized":{
+         * "en_US":"Firsty"
+         * },
+         * "preferredLocale":{
+         * "country":"US",
+         * "language":"en"
+         * }
+         * },
+         * "id":"t7xZ7s4F02"
+         * }
+         *  With 'preferredLocale' being optional
+         */
+        $attributes = [
+            $prefix . "id" => [$resourceOwnerAttributes["id"]]
+        ];
+        foreach (['firstName', 'lastName'] as $attributeName) {
+            $value = $this->getFirstValueFromMultiLocaleString($attributeName, $resourceOwnerAttributes);
+            if ($value) {
+                $attributes[$prefix . $attributeName] = [$value];
+            }
+        }
+
+
+        return $attributes;
+    }
+
+    /**
+     * LinkedIn's attribute values are complex subobjects per
+     * https://docs.microsoft.com/en-us/linkedin/shared/references/v2/object-types#multilocalestring
+     * @param string $attributeName The multiLocalString attribute to check
+     * @param array $attributes All the linkedIn attributes
+     * @return string|false|null Return the first value or null/false if there is no value
+     */
+    private function getFirstValueFromMultiLocaleString($attributeName, array $attributes)
+    {
+        if (isset($attributes[$attributeName]['localized'])) {
+            // reset gives us the first value from the multi valued associate localized array
+            return reset($attributes[$attributeName]['localized']);
+        }
+        return null;
+    }
+
+
+    /**
+     * Query LinkedIn's email endpoint if needed.
+     * Public for testing
+     * @param AccessToken $accessToken
+     * @param AbstractProvider $provider
+     * @param array $state
+     */
+    public function postFinalStep(AccessToken $accessToken, AbstractProvider $provider, &$state)
+    {
+        if (!in_array('r_emailaddress', $this->config->getArray('scopes'))) {
+            // We didn't request email scope originally
+            return;
+        }
+        $emailUrl = $this->getConfig()->getString('urlResourceOwnerEmail');
+        $request = $provider->getAuthenticatedRequest('GET', $emailUrl, $accessToken);
+        try {
+            $response = $this->retry(
+                function () use ($provider, $request) {
+                    return $provider->getParsedResponse($request);
+                },
+                $this->config->getInteger('retryOnError', 1)
+            );
+        } catch (\Exception $e) {
+            // not getting email shouldn't fail the authentication
+            Logger::error(
+                'linkedInv2Auth: ' . $this->getLabel() . ' exception email query response ' . $e->getMessage()
+            );
+            return;
+        }
+
+        if (is_array($response) && isset($response["elements"][0]["handle~"]["emailAddress"])) {
+            /**
+             * A valid response for email lookups is:
+             * {
+             * "elements" : [ {
+             * "handle" : "urn:li:emailAddress:5266785132",
+             * "handle~" : {
+             * "emailAddress" : "patrick+testuser@cirrusidentity.com"
+             * }
+             * } ]
+             * }
+             */
+            $prefix = $this->getAttributePrefix();
+            $state['Attributes'][$prefix . 'emailAddress'] = [$response["elements"][0]["handle~"]["emailAddress"]];
+        } else {
+            Logger::error(
+                'linkedInv2Auth: ' . $this->getLabel() . ' invalid email query response ' . var_export($response, true)
+            );
+        }
+    }
+}
diff --git a/lib/Auth/Source/MicrosoftHybridAuth.php b/lib/Auth/Source/MicrosoftHybridAuth.php
index f6451afcfeac17f7bd71c13e82f067bcc344ca3e..78a523888b8b91bda2fab0d05452c021398f9f4c 100644
--- a/lib/Auth/Source/MicrosoftHybridAuth.php
+++ b/lib/Auth/Source/MicrosoftHybridAuth.php
@@ -46,7 +46,7 @@ class MicrosoftHybridAuth extends OAuth2
         }
 
         $idTokenData = $this->extraIdTokenAttributes($accessToken->getValues()['id_token']);
-        $prefix = $this->config->getString('attributePrefix', '');
+        $prefix = $this->getAttributePrefix();
 
         if (array_key_exists('email', $idTokenData)) {
             $state['Attributes'][$prefix . 'mail'] = [$idTokenData['email']];
diff --git a/lib/Auth/Source/OAuth2.php b/lib/Auth/Source/OAuth2.php
index a7f5c8b010882a5d3e68202ab129a8efabcf3902..e56b6e938d1e270ae7a6f019f1ab39e746ee490c 100644
--- a/lib/Auth/Source/OAuth2.php
+++ b/lib/Auth/Source/OAuth2.php
@@ -7,16 +7,15 @@ use GuzzleHttp\Exception\ConnectException;
 use GuzzleHttp\HandlerStack;
 use GuzzleHttp\MessageFormatter;
 use GuzzleHttp\Middleware;
-use League\OAuth2\Client\Provider\GenericProvider;
 use League\OAuth2\Client\Provider\AbstractProvider;
+use League\OAuth2\Client\Provider\ResourceOwnerInterface;
 use League\OAuth2\Client\Token\AccessToken;
-use SAML2\Utils;
 use SimpleSAML\Logger;
+use SimpleSAML\Module;
 use SimpleSAML\Module\authoauth2\AttributeManipulator;
 use SimpleSAML\Module\authoauth2\ConfigTemplate;
 use SimpleSAML\Module\authoauth2\Providers\AdjustableGenericProvider;
 use SimpleSAML\Module\authoauth2\PsrLogBridge;
-use SimpleSAML\Utils\Arrays;
 use SimpleSAML\Utils\HTTP;
 
 /**
@@ -69,7 +68,7 @@ class OAuth2 extends \SimpleSAML_Auth_Source
             }
         }
         if (!array_key_exists('redirectUri', $config)) {
-            $config['redirectUri'] = \SimpleSAML\Module::getModuleURL('authoauth2/linkback.php');
+            $config['redirectUri'] = Module::getModuleURL('authoauth2/linkback.php');
         }
         if (!array_key_exists('timeout', $config)) {
             $config['timeout'] = 3;
@@ -190,6 +189,7 @@ class OAuth2 extends \SimpleSAML_Auth_Source
             Logger::debug('authoauth2: ' . $providerLabel . ' id_token json: ' . $decodedIdToken);
         }
 
+        /** @var ResourceOwnerInterface $resourceOwner */
         $resourceOwner = $this->retry(
             function () use ($provider, $accessToken) {
                 return $provider->getResourceOwner($accessToken);
@@ -198,9 +198,8 @@ class OAuth2 extends \SimpleSAML_Auth_Source
         );
 
         $attributes = $resourceOwner->toArray();
-        $prefix = $this->config->getString('attributePrefix', '');
-        $attributeManipulator = new AttributeManipulator();
-        $state['Attributes'] = $attributeManipulator->prefixAndFlatten($attributes, $prefix);
+        $prefix = $this->getAttributePrefix();
+        $state['Attributes'] = $this->convertResourceOwnerAttributes($attributes, $prefix);
         $this->postFinalStep($accessToken, $provider, $state);
         Logger::debug(
             'authoauth2: ' . $providerLabel . ' attributes: ' . implode(", ", array_keys($state['Attributes']))
@@ -210,6 +209,19 @@ class OAuth2 extends \SimpleSAML_Auth_Source
         Logger::debug('authoauth2: ' . $providerLabel . ' finished authentication in ' . $time . ' seconds');
     }
 
+    /**
+     * Take the array of users attributes from the Oauth2 provider and convert them into a form usable by SSP.
+     * The default implementation attempts to flatten the user attribute structure and prefix the attribute names
+     * @param array $resourceOwnerAttributes The array of attributes from the OAuth2/OIDC provider
+     * @param string $prefix A string to put in front of all attribute names
+     * @return array The SSP attributes, in form suitable to assign to $state['Attributes']
+     */
+    protected function convertResourceOwnerAttributes(array $resourceOwnerAttributes, $prefix)
+    {
+        $attributeManipulator = new AttributeManipulator();
+        return $attributeManipulator->prefixAndFlatten($resourceOwnerAttributes, $prefix);
+    }
+
     /**
      * Retry token and user info endpoints in event of network errors.
      * @param callable $function the function to try
@@ -290,4 +302,9 @@ class OAuth2 extends \SimpleSAML_Auth_Source
     {
         return $this->config;
     }
+
+    protected function getAttributePrefix()
+    {
+        return $this->config->getString('attributePrefix', '');
+    }
 }
diff --git a/lib/ConfigTemplate.php b/lib/ConfigTemplate.php
index 378e4ee01666f5d5b083150198959666a6a9c177..bc9d808efeb8a8167deb9ecb88adcb98f9706be1 100644
--- a/lib/ConfigTemplate.php
+++ b/lib/ConfigTemplate.php
@@ -44,6 +44,7 @@ class ConfigTemplate
         'label' => 'google'
     ];
 
+    // Deprecated
     const LinkedIn = [
         'authoauth2:OAuth2',
         // *** LinkedIn Endpoints ***
@@ -58,6 +59,25 @@ class ConfigTemplate
         'label' => 'linkedin'
     ];
 
+    const LinkedInV2 = [
+        'authoauth2:LinkedInV2Auth',
+        // *** LinkedIn Endpoints ***
+        'urlAuthorize' => 'https://www.linkedin.com/oauth/v2/authorization',
+        'urlAccessToken' => 'https://www.linkedin.com/oauth/v2/accessToken',
+        'urlResourceOwnerDetails' => 'https://api.linkedin.com/v2/me',
+        'urlResourceOwnerEmail' => 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))',
+        //scopes are the default ones configured for your application
+        'attributePrefix' => 'linkedin.',
+        'scopes' => [
+            'r_liteprofile',
+            // This requires additional api call to the urlResourceOwnerEmail url
+            'r_emailaddress',
+        ],
+        'scopeSeparator' => ' ',
+        // Improve log lines
+        'label' => 'linkedin'
+    ];
+
     //https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc
     //https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
     // WARNING: The OIDC user resource endpoint only returns sub, which is a targeted id.
@@ -115,6 +135,7 @@ class ConfigTemplate
         'label' => 'yahoo'
     ];
 
+    // TODO: weibo is work in progress
     const Weibo = [
         'authoauth2:OAuth2',
         // *** Weibo Endpoints ***
diff --git a/lib/OAuth2ResponseHandler.php b/lib/OAuth2ResponseHandler.php
index 9735e85a7e718c855576c0bfe12dbad1b0b883b7..0d981589580e8e32bd83299568e74fca90b1260b 100644
--- a/lib/OAuth2ResponseHandler.php
+++ b/lib/OAuth2ResponseHandler.php
@@ -25,7 +25,13 @@ class OAuth2ResponseHandler
      * 'access_denied' is OAuth2 standard. Some AS made up their own codes, so support the common ones.
      * @var string[]
      */
-    private $errorsUserConsent = ['access_denied', 'user_denied', 'user_cancelled_authorize', 'consent_required', 'user_cancelled_login'];
+    private $errorsUserConsent = [
+        'access_denied',
+        'user_denied',
+        'user_cancelled_authorize',
+        'consent_required',
+        'user_cancelled_login'
+    ];
 
     /**
      * Look at the state parameter returned by the OAuth2 server and determine if we can handle it;
diff --git a/tests/lib/Auth/Source/LinkedInV2AuthTest.php b/tests/lib/Auth/Source/LinkedInV2AuthTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..6a66e17cc901f20648166f130274c4fb9f019190
--- /dev/null
+++ b/tests/lib/Auth/Source/LinkedInV2AuthTest.php
@@ -0,0 +1,136 @@
+<?php
+
+namespace Test\SimpleSAML\Auth\Source;
+
+use League\OAuth2\Client\Provider\AbstractProvider;
+use League\OAuth2\Client\Token\AccessToken;
+use Psr\Http\Message\RequestInterface;
+use SimpleSAML\Module\authoauth2\Auth\Source\LinkedInV2Auth;
+
+class LinkedInV2AuthTest extends \PHPUnit_Framework_TestCase
+{
+    public static function setUpBeforeClass()
+    {
+        putenv('SIMPLESAMLPHP_CONFIG_DIR=' . dirname(dirname(dirname(__DIR__))) . '/config');
+    }
+
+    /**
+     * Confirms linkedIn's attribute structure gets converted correctly
+     * @dataProvider attributeConversionProvider
+     * @param array $userAttributes The attributes from the linkedIn endpoint
+     * @param array $expectedAttributes The expected attributes
+     */
+    public function testAttributeConversion(array $userAttributes, array $expectedAttributes)
+    {
+        $linkedInAuth = new LinkedInV2Auth(['AuthId' => 'linked'], []);
+        $attributes = $linkedInAuth->convertResourceOwnerAttributes($userAttributes, 'linkedin.');
+        $this->assertEquals($expectedAttributes, $attributes);
+    }
+
+    public function attributeConversionProvider()
+    {
+        return [
+            [["id" => "abc"], ["linkedin.id" => ["abc"]]],
+            [
+                [
+                    "id" => "abc",
+                    "firstName" => ["localized" => ["en_US" => "Jon", "en_CA" => "John"]],
+                    "lastName" => ['not-used']
+                ],
+                ["linkedin.id" => ["abc"], 'linkedin.firstName' => ["Jon"]]
+            ],
+            [
+                [
+                    "id" => "abc",
+                    "firstName" => ["localized" => ["en_US" => "Jon", "en_CA" => "John"]],
+                    "lastName" => ["localized" => ["en_CA" => "Smith"]],
+                ],
+                ["linkedin.id" => ["abc"], 'linkedin.firstName' => ["Jon"], 'linkedin.lastName' => ["Smith"]]
+            ],
+        ];
+    }
+
+    public function testNoEmailCallIfNotRequested()
+    {
+        $linkedInAuth = new LinkedInV2Auth(['AuthId' => 'linked'], ['scopes' => ['r_liteprofile']]);
+        $state = [];
+        /**
+         * @var $mock AbstractProvider|\PHPUnit_Framework_MockObject_MockObject
+         */
+        $mock = $this->getMockBuilder(AbstractProvider::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $mock->expects($this->never())
+            ->method('getAuthenticatedRequest');
+        $linkedInAuth->postFinalStep(new AccessToken(['access_token' => 'abc']), $mock, $state);
+
+        $this->assertEquals([], $state, "State array not changed");
+    }
+
+    /**
+     * @dataProvider getEmailProvider
+     * @param array $emailResponse The response from the email endpoint
+     * @param array $expectedAttributes What the SSP attributes are expected to be
+     */
+    public function testGettingEmail(array $emailResponse, array $expectedAttributes)
+    {
+        $linkedInAuth = new LinkedInV2Auth(['AuthId' => 'linked'], []);
+        $state = [
+            'Attributes' => [
+                'linkedin.id' => ['abc']
+            ]
+        ];
+
+        $token = new AccessToken(['access_token' => 'abc']);
+        /**
+         * @var $mock AbstractProvider|\PHPUnit_Framework_MockObject_MockObject
+         */
+        $mock = $this->getMockBuilder(AbstractProvider::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $mockRequest = $this->createMock(RequestInterface::class);
+        $mock->method('getAuthenticatedRequest')
+            ->with('GET', 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))', $token)
+            ->willReturn($mockRequest);
+
+        $mock->method('getParsedResponse')
+            ->with($mockRequest)
+            ->willReturn($emailResponse);
+        $linkedInAuth->postFinalStep($token, $mock, $state);
+
+        $this->assertEquals(
+            $expectedAttributes,
+            $state['Attributes'],
+            "mail should be added"
+        );
+    }
+
+    public function getEmailProvider()
+    {
+        return [
+            [
+                // valid email response
+                [
+                    "elements" => [
+                        [
+                            "handle" => "urn:li:emailAddress:5266785132",
+                            "handle~" => [
+                                "emailAddress" => "testuser@cirrusidentity.com"
+                            ]
+                        ]
+                    ]
+                ],
+                // email added
+                ['linkedin.id' => ['abc'], 'linkedin.emailAddress' => ['testuser@cirrusidentity.com']]
+            ],
+            [
+                [
+                    'someerror' => 'errormessage'
+                ],
+                // email not added
+                ['linkedin.id' => ['abc']],
+            ],
+        ];
+    }
+}