/* * Greenshot - a free and open source screenshot tool * Copyright (C) 2007-2020 Thomas Braun, Jens Klingen, Robin Krom * * For more information see: http://getgreenshot.org/ * The Greenshot project is hosted on GitHub https://github.com/greenshot/greenshot * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 1 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ using System; using System.Collections.Generic; using System.Drawing; using System.Net; using GreenshotPlugin.Controls; namespace GreenshotPlugin.Core.OAuth { /// /// Code to simplify OAuth 2 /// public static class OAuth2Helper { private const string RefreshToken = "refresh_token"; private const string AccessToken = "access_token"; private const string Code = "code"; private const string Error = "error"; private const string ClientId = "client_id"; private const string ClientSecret = "client_secret"; private const string GrantType = "grant_type"; private const string AuthorizationCode = "authorization_code"; private const string RedirectUri = "redirect_uri"; private const string ExpiresIn = "expires_in"; /// /// Generate an OAuth 2 Token by using the supplied code /// /// OAuth2Settings to update with the information that was retrieved public static void GenerateRefreshToken(OAuth2Settings settings) { IDictionary data = new Dictionary { // Use the returned code to get a refresh code { Code, settings.Code }, { ClientId, settings.ClientId }, { ClientSecret, settings.ClientSecret }, { GrantType, AuthorizationCode } }; foreach (string key in settings.AdditionalAttributes.Keys) { data.Add(key, settings.AdditionalAttributes[key]); } HttpWebRequest webRequest = NetworkHelper.CreateWebRequest(settings.TokenUrl, HTTPMethod.POST); NetworkHelper.UploadFormUrlEncoded(webRequest, data); string accessTokenJsonResult = NetworkHelper.GetResponseAsString(webRequest, true); IDictionary refreshTokenResult = JSONHelper.JsonDecode(accessTokenJsonResult); if (refreshTokenResult.ContainsKey("error")) { if (refreshTokenResult.ContainsKey("error_description")) { throw new Exception($"{refreshTokenResult["error"]} - {refreshTokenResult["error_description"]}"); } throw new Exception((string)refreshTokenResult["error"]); } // gives as described here: https://developers.google.com/identity/protocols/OAuth2InstalledApp // "access_token":"1/fFAGRNJru1FTz70BzhT3Zg", // "expires_in":3920, // "token_type":"Bearer", // "refresh_token":"1/xEoDL4iW3cxlI7yDbSRFYNG01kVKM2C-259HOF2aQbI" if (refreshTokenResult.ContainsKey(AccessToken)) { settings.AccessToken = (string)refreshTokenResult[AccessToken]; } if (refreshTokenResult.ContainsKey(RefreshToken)) { settings.RefreshToken = (string)refreshTokenResult[RefreshToken]; } if (refreshTokenResult.ContainsKey(ExpiresIn)) { object seconds = refreshTokenResult[ExpiresIn]; if (seconds != null) { settings.AccessTokenExpires = DateTimeOffset.Now.AddSeconds((double)seconds); } } settings.Code = null; } /// /// Used to update the settings with the callback information /// /// OAuth2Settings /// IDictionary /// true if the access token is already in the callback private static bool UpdateFromCallback(OAuth2Settings settings, IDictionary callbackParameters) { if (!callbackParameters.ContainsKey(AccessToken)) { return false; } if (callbackParameters.ContainsKey(RefreshToken)) { // Refresh the refresh token :) settings.RefreshToken = callbackParameters[RefreshToken]; } if (callbackParameters.ContainsKey(ExpiresIn)) { var expiresIn = callbackParameters[ExpiresIn]; settings.AccessTokenExpires = DateTimeOffset.MaxValue; if (expiresIn != null) { if (double.TryParse(expiresIn, out var seconds)) { settings.AccessTokenExpires = DateTimeOffset.Now.AddSeconds(seconds); } } } settings.AccessToken = callbackParameters[AccessToken]; return true; } /// /// Go out and retrieve a new access token via refresh-token with the TokenUrl in the settings /// Will update the access token, refresh token, expire date /// /// public static void GenerateAccessToken(OAuth2Settings settings) { IDictionary data = new Dictionary { { RefreshToken, settings.RefreshToken }, { ClientId, settings.ClientId }, { ClientSecret, settings.ClientSecret }, { GrantType, RefreshToken } }; foreach (string key in settings.AdditionalAttributes.Keys) { data.Add(key, settings.AdditionalAttributes[key]); } HttpWebRequest webRequest = NetworkHelper.CreateWebRequest(settings.TokenUrl, HTTPMethod.POST); NetworkHelper.UploadFormUrlEncoded(webRequest, data); string accessTokenJsonResult = NetworkHelper.GetResponseAsString(webRequest, true); // gives as described here: https://developers.google.com/identity/protocols/OAuth2InstalledApp // "access_token":"1/fFAGRNJru1FTz70BzhT3Zg", // "expires_in":3920, // "token_type":"Bearer", IDictionary accessTokenResult = JSONHelper.JsonDecode(accessTokenJsonResult); if (accessTokenResult.ContainsKey("error")) { if ("invalid_grant" == (string)accessTokenResult["error"]) { // Refresh token has also expired, we need a new one! settings.RefreshToken = null; settings.AccessToken = null; settings.AccessTokenExpires = DateTimeOffset.MinValue; settings.Code = null; return; } if (accessTokenResult.ContainsKey("error_description")) { throw new Exception($"{accessTokenResult["error"]} - {accessTokenResult["error_description"]}"); } throw new Exception((string)accessTokenResult["error"]); } if (accessTokenResult.ContainsKey(AccessToken)) { settings.AccessToken = (string) accessTokenResult[AccessToken]; settings.AccessTokenExpires = DateTimeOffset.MaxValue; } if (accessTokenResult.ContainsKey(RefreshToken)) { // Refresh the refresh token :) settings.RefreshToken = (string)accessTokenResult[RefreshToken]; } if (accessTokenResult.ContainsKey(ExpiresIn)) { object seconds = accessTokenResult[ExpiresIn]; if (seconds != null) { settings.AccessTokenExpires = DateTimeOffset.Now.AddSeconds((double) seconds); } } } /// /// Authorize by using the mode specified in the settings /// /// OAuth2Settings /// false if it was canceled, true if it worked, exception if not public static bool Authorize(OAuth2Settings settings) { var completed = settings.AuthorizeMode switch { OAuth2AuthorizeMode.LocalServer => AuthorizeViaLocalServer(settings), OAuth2AuthorizeMode.EmbeddedBrowser => AuthorizeViaEmbeddedBrowser(settings), OAuth2AuthorizeMode.JsonReceiver => AuthorizeViaDefaultBrowser(settings), _ => throw new NotImplementedException($"Authorize mode '{settings.AuthorizeMode}' is not 'yet' implemented."), }; return completed; } /// /// Authorize via the default browser, via the Greenshot website. /// It will wait for a Json post. /// If this works, return the code /// /// OAuth2Settings with the Auth / Token url etc /// true if completed, false if canceled private static bool AuthorizeViaDefaultBrowser(OAuth2Settings settings) { var codeReceiver = new LocalJsonReceiver(); IDictionary result = codeReceiver.ReceiveCode(settings); if (result == null || result.Count == 0) { return false; } foreach (var key in result.Keys) { switch (key) { case AccessToken: settings.AccessToken = result[key]; break; case ExpiresIn: if (int.TryParse(result[key], out var seconds)) { settings.AccessTokenExpires = DateTimeOffset.Now.AddSeconds(seconds); } break; case RefreshToken: settings.RefreshToken = result[key]; break; } } if (result.TryGetValue("error", out var error)) { if (result.TryGetValue("error_description", out var errorDescription)) { throw new Exception(errorDescription); } if ("access_denied" == error) { throw new UnauthorizedAccessException("Access denied"); } throw new Exception(error); } if (result.TryGetValue(Code, out var code) && !string.IsNullOrEmpty(code)) { settings.Code = code; GenerateRefreshToken(settings); return !string.IsNullOrEmpty(settings.AccessToken); } return true; } /// /// Authorize via an embedded browser /// If this works, return the code /// /// OAuth2Settings with the Auth / Token url etc /// true if completed, false if canceled private static bool AuthorizeViaEmbeddedBrowser(OAuth2Settings settings) { if (string.IsNullOrEmpty(settings.CloudServiceName)) { throw new ArgumentNullException(nameof(settings.CloudServiceName)); } if (settings.BrowserSize == Size.Empty) { throw new ArgumentNullException(nameof(settings.BrowserSize)); } OAuthLoginForm loginForm = new OAuthLoginForm($"Authorize {settings.CloudServiceName}", settings.BrowserSize, settings.FormattedAuthUrl, settings.RedirectUrl); loginForm.ShowDialog(); if (!loginForm.IsOk) return false; if (loginForm.CallbackParameters.TryGetValue(Code, out var code) && !string.IsNullOrEmpty(code)) { settings.Code = code; GenerateRefreshToken(settings); return true; } return UpdateFromCallback(settings, loginForm.CallbackParameters); } /// /// Authorize via a local server by using the LocalServerCodeReceiver /// If this works, return the code /// /// OAuth2Settings with the Auth / Token url etc /// true if completed private static bool AuthorizeViaLocalServer(OAuth2Settings settings) { var codeReceiver = new LocalServerCodeReceiver(); IDictionary result = codeReceiver.ReceiveCode(settings); if (result.TryGetValue(Code, out var code) && !string.IsNullOrEmpty(code)) { settings.Code = code; GenerateRefreshToken(settings); return true; } if (result.TryGetValue("error", out var error)) { if (result.TryGetValue("error_description", out var errorDescription)) { throw new Exception(errorDescription); } if ("access_denied" == error) { throw new UnauthorizedAccessException("Access denied"); } throw new Exception(error); } return false; } /// /// Simple helper to add the Authorization Bearer header /// /// WebRequest /// OAuth2Settings public static void AddOAuth2Credentials(HttpWebRequest webRequest, OAuth2Settings settings) { if (!string.IsNullOrEmpty(settings.AccessToken)) { webRequest.Headers.Add("Authorization", "Bearer " + settings.AccessToken); } } /// /// Check and authenticate or refresh tokens /// /// OAuth2Settings public static void CheckAndAuthenticateOrRefresh(OAuth2Settings settings) { // Get Refresh / Access token if (string.IsNullOrEmpty(settings.RefreshToken)) { if (!Authorize(settings)) { throw new Exception("Authentication cancelled"); } } if (settings.IsAccessTokenExpired) { GenerateAccessToken(settings); // Get Refresh / Access token if (string.IsNullOrEmpty(settings.RefreshToken)) { if (!Authorize(settings)) { throw new Exception("Authentication cancelled"); } GenerateAccessToken(settings); } } if (settings.IsAccessTokenExpired) { throw new Exception("Authentication failed"); } } /// /// CreateWebRequest ready for OAuth 2 access /// /// HTTPMethod /// /// OAuth2Settings /// HttpWebRequest public static HttpWebRequest CreateOAuth2WebRequest(HTTPMethod method, string url, OAuth2Settings settings) { CheckAndAuthenticateOrRefresh(settings); HttpWebRequest webRequest = NetworkHelper.CreateWebRequest(url, method); AddOAuth2Credentials(webRequest, settings); return webRequest; } } }