diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,17 @@
-## [*Unreleased*](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.6.1.7...master)
+## [*Unreleased*](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.6.2.0...master)
 
 None
+
+## [v0.6.2.0](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.6.1.7...v0.6.2.0)
+
+- Filter `+` from `state` tokens
+
+  This decreases entropy in the token slightly, but ensures that providers
+  performing unexpected +/space/%20 encoding (e.g. ClassLink) still function.
+
+  See [#140](https://github.com/thoughtbot/yesod-auth-oauth2/pull/140).
+
+- Add ClassLink provider
 
 ## [v0.6.1.7](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.6.1.6...v0.6.1.7)
 
diff --git a/example/Main.hs b/example/Main.hs
--- a/example/Main.hs
+++ b/example/Main.hs
@@ -38,6 +38,7 @@
 import Yesod.Auth.OAuth2.AzureAD
 import Yesod.Auth.OAuth2.BattleNet
 import Yesod.Auth.OAuth2.Bitbucket
+import Yesod.Auth.OAuth2.ClassLink
 import Yesod.Auth.OAuth2.EveOnline
 import Yesod.Auth.OAuth2.GitHub
 import Yesod.Auth.OAuth2.GitLab
@@ -46,8 +47,8 @@
 import Yesod.Auth.OAuth2.Salesforce
 import Yesod.Auth.OAuth2.Slack
 import Yesod.Auth.OAuth2.Spotify
-import Yesod.Auth.OAuth2.WordPressDotCom
 import Yesod.Auth.OAuth2.Upcase
+import Yesod.Auth.OAuth2.WordPressDotCom
 
 data App = App
     { appHttpManager :: Manager
@@ -73,10 +74,9 @@
 
     -- Copy the Creds response into the session for viewing after
     authenticate c = do
-        mapM_ (uncurry setSession) $
-            [ ("credsIdent", credsIdent c)
-            , ("credsPlugin", credsPlugin c)
-            ] ++ credsExtra c
+        mapM_ (uncurry setSession)
+            $ [("credsIdent", credsIdent c), ("credsPlugin", credsPlugin c)]
+            ++ credsExtra c
 
         return $ Authenticated "1"
 
@@ -138,6 +138,7 @@
         [ loadPlugin oauth2AzureAD "AZURE_AD"
         , loadPlugin (oauth2BattleNet [whamlet|TODO|] "en") "BATTLE_NET"
         , loadPlugin oauth2Bitbucket "BITBUCKET"
+        , loadPlugin oauth2ClassLink "CLASSLINK"
         , loadPlugin (oauth2Eve Plain) "EVE_ONLINE"
         , loadPlugin oauth2GitHub "GITHUB"
         , loadPlugin oauth2GitLab "GITLAB"
@@ -150,7 +151,7 @@
         , loadPlugin oauth2Upcase "UPCASE"
         ]
 
-    return App {..}
+    return App { .. }
   where
     loadPlugin f prefix = do
         clientId <- getEnv $ prefix <> "_CLIENT_ID"
diff --git a/src/Yesod/Auth/OAuth2/ClassLink.hs b/src/Yesod/Auth/OAuth2/ClassLink.hs
new file mode 100644
--- /dev/null
+++ b/src/Yesod/Auth/OAuth2/ClassLink.hs
@@ -0,0 +1,51 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Yesod.Auth.OAuth2.ClassLink
+    ( oauth2ClassLink
+    , oauth2ClassLinkScoped
+    )
+where
+
+import Yesod.Auth.OAuth2.Prelude
+
+import qualified Data.Text as T
+
+newtype User = User Int
+
+instance FromJSON User where
+    parseJSON = withObject "User" $ \o -> User <$> o .: "UserId"
+
+pluginName :: Text
+pluginName = "classlink"
+
+defaultScopes :: [Text]
+defaultScopes = ["profile", "oneroster"]
+
+oauth2ClassLink :: YesodAuth m => Text -> Text -> AuthPlugin m
+oauth2ClassLink = oauth2ClassLinkScoped defaultScopes
+
+oauth2ClassLinkScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
+oauth2ClassLinkScoped scopes clientId clientSecret =
+    authOAuth2 pluginName oauth2 $ \manager token -> do
+        (User userId, userResponse) <- authGetProfile
+            pluginName
+            manager
+            token
+            "https://nodeapi.classlink.com/v2/my/info"
+
+        pure Creds
+            { credsPlugin = pluginName
+            , credsIdent = T.pack $ show userId
+            , credsExtra = setExtra token userResponse
+            }
+  where
+    oauth2 = OAuth2
+        { oauthClientId = clientId
+        , oauthClientSecret = Just clientSecret
+        , oauthOAuthorizeEndpoint =
+            "https://launchpad.classlink.com/oauth2/v2/auth"
+                `withQuery` [scopeParam "," scopes]
+        , oauthAccessTokenEndpoint =
+            "https://launchpad.classlink.com/oauth2/v2/token"
+        , oauthCallback = Nothing
+        }
diff --git a/src/Yesod/Auth/OAuth2/Dispatch.hs b/src/Yesod/Auth/OAuth2/Dispatch.hs
--- a/src/Yesod/Auth/OAuth2/Dispatch.hs
+++ b/src/Yesod/Auth/OAuth2/Dispatch.hs
@@ -156,14 +156,25 @@
 getParentUrlRender :: MonadHandler m => m (Route (SubHandlerSite m) -> Text)
 getParentUrlRender = (.) <$> getUrlRender <*> getRouteToParent
 
--- | Set a random, 30-character value in the session
+-- | Set a random, ~30-character value in the session
+--
+-- Some (but not all) providers decode a @+@ in the state token as a space when
+-- sending it back to us. We don't expect this and fail. And if we did code for
+-- it, we'd then fail on the providers that /don't/ do that.
+--
+-- Therefore, we just exclude @+@ in our tokens, which means this function may
+-- return slightly less than 30 characters.
+--
 setSessionCSRF :: MonadHandler m => Text -> m Text
 setSessionCSRF sessionKey = do
     csrfToken <- liftIO randomToken
     csrfToken <$ setSession sessionKey csrfToken
   where
     randomToken =
-        decodeUtf8 . convertToBase @ByteString Base64 <$> getRandomBytes 64
+        T.filter (/= '+')
+            . decodeUtf8
+            . convertToBase @ByteString Base64
+            <$> getRandomBytes 64
 
 -- | Verify the callback provided the same CSRF token as in our session
 verifySessionCSRF :: MonadHandler m => Text -> m Text
@@ -172,8 +183,14 @@
     sessionToken <- lookupSession sessionKey
     deleteSession sessionKey
 
-    unless (sessionToken == Just token)
-        $ permissionDenied "Invalid OAuth2 state token"
+    unless (sessionToken == Just token) $ do
+        $(logError)
+            $ "state token does not match. "
+            <> "Param: "
+            <> tshow token
+            <> "State: "
+            <> tshow sessionToken
+        permissionDenied "Invalid OAuth2 state token"
 
     return token
 
diff --git a/yesod-auth-oauth2.cabal b/yesod-auth-oauth2.cabal
--- a/yesod-auth-oauth2.cabal
+++ b/yesod-auth-oauth2.cabal
@@ -1,117 +1,110 @@
-cabal-version: 1.12
-
--- This file has been generated from package.yaml by hpack version 0.33.0.
---
--- see: https://github.com/sol/hpack
---
--- hash: 426ae261ba0f32722928565c45da419547ef444e7635ac5e9da56872729a0df9
+cabal-version:      1.12
+name:               yesod-auth-oauth2
+version:            0.6.2.0
+license:            MIT
+license-file:       LICENSE
+maintainer:         Pat Brisbin <pbrisbin@gmail.com>
+author:             Tom Streller
+homepage:           http://github.com/thoughtbot/yesod-auth-oauth2
+bug-reports:        https://github.com/thoughtbot/yesod-auth-oauth2/issues
+synopsis:           OAuth 2.0 authentication plugins
+description:
+    Library to authenticate with OAuth 2.0 for Yesod web applications.
 
-name:           yesod-auth-oauth2
-version:        0.6.1.7
-synopsis:       OAuth 2.0 authentication plugins
-description:    Library to authenticate with OAuth 2.0 for Yesod web applications.
-category:       Web
-homepage:       http://github.com/thoughtbot/yesod-auth-oauth2
-bug-reports:    https://github.com/thoughtbot/yesod-auth-oauth2/issues
-author:         Tom Streller
-maintainer:     Pat Brisbin <pbrisbin@gmail.com>
-license:        MIT
-license-file:   LICENSE
-build-type:     Simple
+category:           Web
+build-type:         Simple
 extra-source-files:
     README.md
     CHANGELOG.md
 
 source-repository head
-  type: git
-  location: https://github.com/thoughtbot/yesod-auth-oauth2
+    type:     git
+    location: https://github.com/thoughtbot/yesod-auth-oauth2
 
 flag example
-  description: Build the example application
-  manual: False
-  default: False
+    description: Build the example application
+    default:     False
 
 library
-  exposed-modules:
-      URI.ByteString.Extension
-      Yesod.Auth.OAuth2
-      Yesod.Auth.OAuth2.AzureAD
-      Yesod.Auth.OAuth2.BattleNet
-      Yesod.Auth.OAuth2.Bitbucket
-      Yesod.Auth.OAuth2.Dispatch
-      Yesod.Auth.OAuth2.ErrorResponse
-      Yesod.Auth.OAuth2.EveOnline
-      Yesod.Auth.OAuth2.Exception
-      Yesod.Auth.OAuth2.GitHub
-      Yesod.Auth.OAuth2.GitLab
-      Yesod.Auth.OAuth2.Google
-      Yesod.Auth.OAuth2.Nylas
-      Yesod.Auth.OAuth2.Prelude
-      Yesod.Auth.OAuth2.Salesforce
-      Yesod.Auth.OAuth2.Slack
-      Yesod.Auth.OAuth2.Spotify
-      Yesod.Auth.OAuth2.Upcase
-      Yesod.Auth.OAuth2.WordPressDotCom
-  other-modules:
-      Paths_yesod_auth_oauth2
-  hs-source-dirs:
-      src
-  ghc-options: -Wall
-  build-depends:
-      aeson >=0.6 && <1.6
-    , base >=4.9.0.0 && <5
-    , bytestring >=0.9.1.4
-    , cryptonite
-    , errors
-    , hoauth2 >=1.11.0 && <1.17
-    , http-client >=0.4.0 && <0.8
-    , http-conduit >=2.0 && <3.0
-    , http-types >=0.8 && <0.13
-    , memory
-    , microlens
-    , safe-exceptions
-    , text >=0.7 && <2.0
-    , uri-bytestring
-    , yesod-auth >=1.6.0 && <1.7
-    , yesod-core >=1.6.0 && <1.7
-  default-language: Haskell2010
+    exposed-modules:
+        URI.ByteString.Extension
+        Yesod.Auth.OAuth2
+        Yesod.Auth.OAuth2.AzureAD
+        Yesod.Auth.OAuth2.BattleNet
+        Yesod.Auth.OAuth2.Bitbucket
+        Yesod.Auth.OAuth2.ClassLink
+        Yesod.Auth.OAuth2.Dispatch
+        Yesod.Auth.OAuth2.ErrorResponse
+        Yesod.Auth.OAuth2.EveOnline
+        Yesod.Auth.OAuth2.Exception
+        Yesod.Auth.OAuth2.GitHub
+        Yesod.Auth.OAuth2.GitLab
+        Yesod.Auth.OAuth2.Google
+        Yesod.Auth.OAuth2.Nylas
+        Yesod.Auth.OAuth2.Prelude
+        Yesod.Auth.OAuth2.Salesforce
+        Yesod.Auth.OAuth2.Slack
+        Yesod.Auth.OAuth2.Spotify
+        Yesod.Auth.OAuth2.Upcase
+        Yesod.Auth.OAuth2.WordPressDotCom
 
+    hs-source-dirs:   src
+    other-modules:    Paths_yesod_auth_oauth2
+    default-language: Haskell2010
+    ghc-options:      -Wall
+    build-depends:
+        aeson >=0.6 && <1.6,
+        base >=4.9.0.0 && <5,
+        bytestring >=0.9.1.4 && <0.11,
+        cryptonite >=0.26 && <0.28,
+        errors >=2.3.0 && <2.4,
+        hoauth2 >=1.11.0 && <1.17,
+        http-client >=0.4.0 && <0.8,
+        http-conduit >=2.0 && <3.0,
+        http-types >=0.8 && <0.13,
+        memory >=0.15.0 && <0.16,
+        microlens >=0.4.11.2 && <0.5,
+        safe-exceptions >=0.1.7.1 && <0.2,
+        text >=0.7 && <2.0,
+        uri-bytestring >=0.3.3.0 && <0.4,
+        yesod-auth >=1.6.0 && <1.7,
+        yesod-core >=1.6.0 && <1.7
+
 executable yesod-auth-oauth2-example
-  main-is: Main.hs
-  other-modules:
-      Paths_yesod_auth_oauth2
-  hs-source-dirs:
-      example
-  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
-  build-depends:
-      aeson
-    , aeson-pretty
-    , base >=4.9.0.0 && <5
-    , bytestring
-    , containers
-    , http-conduit
-    , load-env
-    , text
-    , warp
-    , yesod
-    , yesod-auth
-    , yesod-auth-oauth2
-  if !(flag(example))
-    buildable: False
-  default-language: Haskell2010
+    main-is:          Main.hs
+    hs-source-dirs:   example
+    other-modules:    Paths_yesod_auth_oauth2
+    default-language: Haskell2010
+    ghc-options:      -Wall -threaded -rtsopts -with-rtsopts=-N
+    build-depends:
+        aeson >=1.4.7.1 && <1.5,
+        aeson-pretty >=0.8.8 && <0.9,
+        base >=4.9.0.0 && <5,
+        bytestring >=0.10.10.1 && <0.11,
+        containers >=0.6.2.1 && <0.7,
+        http-conduit >=2.3.7.4 && <2.4,
+        load-env >=0.2.1.0 && <0.3,
+        text >=1.2.4.0 && <1.3,
+        warp >=3.3.13 && <3.4,
+        yesod >=1.6.1.0 && <1.7,
+        yesod-auth >=1.6.10.1 && <1.7,
+        yesod-auth-oauth2 -any
 
+    if !flag(example)
+        buildable: False
+
 test-suite test
-  type: exitcode-stdio-1.0
-  main-is: Spec.hs
-  other-modules:
-      URI.ByteString.ExtensionSpec
-      Paths_yesod_auth_oauth2
-  hs-source-dirs:
-      test
-  ghc-options: -Wall
-  build-depends:
-      base >=4.9.0.0 && <5
-    , hspec
-    , uri-bytestring
-    , yesod-auth-oauth2
-  default-language: Haskell2010
+    type:             exitcode-stdio-1.0
+    main-is:          Spec.hs
+    hs-source-dirs:   test
+    other-modules:
+        URI.ByteString.ExtensionSpec
+        Paths_yesod_auth_oauth2
+
+    default-language: Haskell2010
+    ghc-options:      -Wall
+    build-depends:
+        base >=4.9.0.0 && <5,
+        hspec >=2.7.4 && <2.8,
+        uri-bytestring >=0.3.3.0 && <0.4,
+        yesod-auth-oauth2 -any
