Giter VIP home page Giter VIP logo

authentication-unity's Introduction

Authentication Package for Unity

Install via Unity Package Manager:

  • Add "com.cdm.authentication": "https://github.com/cdmvision/authentication-unity.git#1.2.0" to your project's package manifest file in dependencies section.
  • Or, Package Manager > Add package from git URL... and paste this URL: https://github.com/cdmvision/authentication-unity.git#1.2.0

Example usage

You should create your client auth configuration:

// Also you can use your own client configuration.
var auth = new GoogleAuth()
{
  clientId = "...",
  redirectUrl = "...",
  scope = "openid email profile"
};

Authentication session is created with auth configuration and a browser:

using var authenticationSession = new AuthenticationSession(auth, new StandaloneBrowser());

Also you can use different browsers for each platform by using cross platform browser:

var crossPlatformBrowser = new CrossPlatformBrowser();
var crossPlatformBrowser.platformBrowsers.Add(RuntimePlatform.WindowsEditor, new StandaloneBrowser());
var crossPlatformBrowser.platformBrowsers.Add(RuntimePlatform.WindowsPlayer, new StandaloneBrowser());
var crossPlatformBrowser.platformBrowsers.Add(RuntimePlatform.OSXEditor, new StandaloneBrowser());
var crossPlatformBrowser.platformBrowsers.Add(RuntimePlatform.OSXPlayer, new StandaloneBrowser());
var crossPlatformBrowser.platformBrowsers.Add(RuntimePlatform.IPhonePlayer, new ASWebAuthenticationSessionBrowser());

using var authenticationSession = new AuthenticationSession(auth, crossPlatformBrowser);

// Opens a browser to log user in
AccessTokenResponse accessTokenResponse = await authenticationSession.AuthenticateAsync();

// Authentication header can be used to make authorized http calls.
AuthenticationHeaderValue authenticationHeader = accessTokenResponse.GetAuthenticationHeader();

// Gets the current acccess token, or refreshes if it is expired.
accessTokenResponse = await authenticationSession.GetOrRefreshTokenAsync();

// Gets new access token by using the refresh token.
AccessTokenResponse newAccessTokenResponse = await authenticationSession.RefreshTokenAsync();

// Or you can get new access token with specified refresh token (i.e. stored on the local disk to prevent multiple sign-in for each app launch)
newAccessTokenResponse = await authenticationSession.RefreshTokenAsync("my_refresh_token");

authentication-unity's People

Contributors

ibrahimpenekli avatar matmespino avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

authentication-unity's Issues

Password Grant type

I was needing a password grant type because of automatic testing.

I leave it here in case anyone can use it. It works with Unity and with Unity UTF.
In the middle term, it would be great to add this grant type to the whole design

    public class OAuth2PasswordGrantManager : IDisposable
    {
        private readonly string authUrl;
        private readonly string clientId;
        private readonly string clientSecret;
        private HttpClient client;

        public OAuth2PasswordGrantManager(string authUrl, string clientId, string clientSecret) :
            this(authUrl, clientId, clientSecret, new HttpClientHandler() { UseProxy = false })
        { }

        internal OAuth2PasswordGrantManager(string authUrl, string clientId, string clientSecret, HttpMessageHandler handler)
        {
            this.authUrl = authUrl;
            this.clientId = clientId;
            this.clientSecret = clientSecret;
            client = new HttpClient(handler);
        }

        public async Task<TokenResponse> GetToken(string username, string password)
        {
            //Build the payload
            Dictionary<string, string> payload = new Dictionary<string, string>
        {
            { "grant_type", "password" },
            { "username", username },
            { "password", password },
            { "client_id", clientId },
            { "client_secret", clientSecret }
        };

            return await Post(payload);
        }

        public async Task<TokenResponse> Refresh(string refreshToken)
        {
            //Build the payload
            Dictionary<string, string> payload = new Dictionary<string, string>
        {
            { "grant_type", "refresh_token" },
            { "refresh_token", refreshToken },
            { "client_id", clientId },
            { "client_secret", clientSecret }
        };

            return await Post(payload);
        }

        private async Task<TokenResponse> Post(Dictionary<string, string> payload)
        {
            HttpResponseMessage response = await client.PostAsync(authUrl, new FormUrlEncodedContent(payload));
            response.EnsureSuccessStatusCode();
            var jsonResponse = await response.Content.ReadAsStringAsync();

            return JsonUtility.FromJson<TokenResponse>(jsonResponse);
        }

        public void Dispose()
        {
            client.Dispose();
        }
    } 

    public class TokenResponse
    {
        public string AccessToken => access_token;
        public int ExpiresIn => expires_in;
        public string TokenType => token_type;
        public string RefreshToken => refresh_token;
        public string Scope => scope;

        [SerializeField]
        internal string access_token;

        [SerializeField]
        internal int expires_in;

        [SerializeField]
        internal string token_type;

        [SerializeField]
        internal string refresh_token;

        [SerializeField]
        internal string scope;
    } 

WebGL support

While reviewing this package for a project that uses WebGL; I found that it works splendidly on Desktop, but when in WebGL it won't work.

After some digging, I found two main culprits: HTTPListener and HTTPClient.

HTTPListener won't work; but I managed to find a way around that by opening a popup with the authorization screen, and sending a message back to the Unity client when it returns. This was inspired by https://developer.okta.com/blog/2021/02/26/unity-webgl-playfab-authorization.

The HTTPClient bit is a bit trickier; this is used in the AuthorizationCodeFlow and UserInfoParser. And at this point I am not sure whether it could be replaced by Unity's UnityWebRequest. It would mean a change in all Clients, as they are based on the AuthorizationCodeFlow.

Have you ever got this to work? Would UnityWebRequest be a viable alternative?

Don´t working with my Website Address (SocketException: The requested address is not valid in the context.)

I try to Put my own site address (valid) to Validity Login, but, just with a localhost link works , any valid (public) link it´s not valid, and show this error.
SocketException: The requested address is not valid in the context.


System.Net.Sockets.Socket.Bind (System.Net.EndPoint localEP) (at <bbae6af52a2a4bd982f8fde9721d2c3b>:0)
System.Net.EndPointListener..ctor (System.Net.HttpListener listener, System.Net.IPAddress addr, System.Int32 port, System.Boolean secure) (at <bbae6af52a2a4bd982f8fde9721d2c3b>:0)
System.Net.EndPointManager.GetEPListener(System.String host, System.Int32 port, System.Net.HttpListener listener, System.Boolean secure) (at <bbae6af52a2a4bd982f8fde9721d2c3b>:0)
System.Net.EndPointManager.RemovePrefixInternal(System.String prefix, System.Net.HttpListener listener) (at <bbae6af52a2a4bd982f8fde9721d2c3b>:0)
System.Net.EndPointManager.RemoveListener (System.Net.HttpListener listener) (at <bbae6af52a2a4bd982f8fde9721d2c3b>:0)
System.Net.HttpListener.Close (System.Boolean force) (at <bbae6af52a2a4bd982f8fde9721d2c3b>:0)
System.Net.HttpListener.System.IDisposable.Dispose() (at <bbae6af52a2a4bd982f8fde9721d2c3b>:0)
Cdm.Authentication.Browser.StandaloneBrowser.StartAsync (System.String loginUrl, System.String redirectUrl, System.Threading.CancellationToken cancelationToken) (in Library/PackageCache/com.cdm.authentication@0ec72dcc62/Runtime/Browser/StandaloneBrowser.cs:49 )
CDM.Authentication.brow.crossplatFormBrowser.startasync (System.String Loginurl, System.String Redirecturl, System.threading.CancellatiToken CancelatiToken) Runtime/browser/crossplatformbrowser.cs: 24 )
Cdm.Authentication.OAuth2.AuthenticationSession.AuthenticateAsync (System.Threading.CancellationToken cancelToken) (in Library/PackageCache/com.cdm.authentication@0ec72dcc62/Runtime/OAuth2/AuthenticationSession.cs:65)```

Can't make it work for UWP

Hi! I want my app to request a permission to Google Drive, so here is my working link to start auth.
https://accounts.google.com/o/oauth2/auth?response_type=code&scope=https://www.googleapis.com/auth/drive&redirect_uri=ps.uwp%3A%2Foauth2callback&client_id=988157355771-r7j9rr6jf4cj7kn8de4pco4vtbbktmb9.apps.googleusercontent.com

I'm trying to use this project to perform auth, but DeepLinkBrowser simply opens https://accounts.google.com/o/oauth2/auth where I get "Required parameter is missing: response_type". Can you please point me to the right direction?

image

AuthorizationUrl missing parameters on Unity 2020 UWP

AuthorizationCodeFlow::GetAuthorizationUrl() is missing parameters when the application is deployed to UWP using Unity 2020

Step to reproduce

  1. Deploy to a UWP application using Unity 2020.3.41
  2. See AuthorizationCodeFlow::AuthorizationUrl()
  3. Check that several parameters are missing, such as required_type and client_id

Causes

GetAuthorizationUrl() uses JsonHelper.ToDictionary.
ToDictionary uses JObject.FromObject(obj).ToObject<Dictionary<string, string>>();
JObject.FromObject(obj) somehow doesn't work properly on UWP 2020.3.41, skipping some parameters

Remarks

JObject.FromObject(obj) skips some paremeters, so they are not added to the dictionary, and, therefore, into the parameters.
This behaviour is very specific, because I have been able to reproduce only on Unity 2020.3.41 UWP when the class is into the package folder, so I guess that has to be something about getting the fields by reflection when the class is in the packages

Workarrounds and fixes

  • Unity 2021 seems to work
  • Move the code outside the package folder
  • Override JsonHelper.ToDictionary to force building the dictionary correctly
        public static Dictionary<string, string> ToDictionary(object obj)
        {
            var dictionary = new Dictionary<string, string>();

            if (obj is AuthorizationCodeRequest authorizationCodeRequest)
            {
                dictionary["response_type"] = authorizationCodeRequest.responseType;
                dictionary["client_id"] = authorizationCodeRequest.clientId;
                dictionary["redirect_uri"] = authorizationCodeRequest.redirectUri;
                dictionary["scope"] = authorizationCodeRequest.scope;
                dictionary["state"] = authorizationCodeRequest.state;
            }
            else if (obj is AccessTokenRequest accessTokenRequest)
            {
                dictionary["grant_type"] = "authorization_code";
                dictionary["code"] = accessTokenRequest.code;
                dictionary["client_id"] = accessTokenRequest.clientId;
                dictionary["client_secret"] = accessTokenRequest.clientSecret;
                dictionary["redirect_uri"] = accessTokenRequest.redirectUri;
            }
            else if (obj is RefreshTokenRequest refreshToken)
            {
                dictionary["grant_type"] = "refresh_token";
                dictionary["refresh_token"] = refreshToken.refreshToken;
                dictionary["scope"] = refreshToken.scope;
            }
            else
            {
                dictionary = JObject.FromObject(obj).ToObject<Dictionary<string, string>>();
            }

            if (dictionary != null)
            {
                // Remove empty parameters.
                var keys = dictionary.Keys.Where(key => string.IsNullOrEmpty(dictionary[key])).ToArray();
                foreach (var key in keys)
                {
                    dictionary.Remove(key);
                }
            }

            return dictionary;
        }

Test and screenshot

I've created a test project with this issue.
The serializable class is on a custom package inside package folder
The test code is this:

using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.UI;

public class TestDataContract : MonoBehaviour
{
    public InputField inputField;
    private void Start()
    {
        var request = new AuthorizationCodeRequest()
        {
            clientId = "john doe",
            redirectUri  = "http://localhost"
        };

        var dictionary = ToDictionary(request);

        StringBuilder builder = new StringBuilder();
        builder.AppendLine($"Dictionary Count: {dictionary.Count}");

        foreach (var kvp in dictionary)
        {
            builder.AppendLine($"Key: {kvp.Key} Value: {kvp.Value}");
        }

        inputField.text = builder.ToString();
    }

    public static Dictionary<string, string> ToDictionary(object obj)
    {
        var dictionary = JObject.FromObject(obj).ToObject<Dictionary<string, string>>();

        if (dictionary != null)
        {
            // Remove empty parameters.
            var keys = dictionary.Keys.Where(key => string.IsNullOrEmpty(dictionary[key])).ToArray();
            foreach (var key in keys)
            {
                dictionary.Remove(key);
            }
        }

        return dictionary;
    }
}

On Editor:
image

On UWP
image

forwardSlash issue with httplistener

Hi,

First, thank you for this package, incredible work and very nice implementation.

I have an issue with urls without forward slashes

Some times, the redirect_url is given without forward slash. However, HttpListener mandatorily needs forward slash

Prefixes must end in a forward slash ("/")https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistener?view=net-7.0

For example, a http://localhost/login will fail and cause an exception because of the httplistener forward slash necessity.

However, this redirect_url is given by the server settings, so there's nothing from client we can do to ask for a forward slashed version http://localhost/login/ in that case

TLDR, my proporsal is to add the forward slash to the http listener

using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

namespace Cdm.Authentication.Browser
{
    /// <summary>
    /// OAuth 2.0 verification browser that runs a local server and waits for a call with
    /// the authorization verification code.
    /// </summary>
    public class StandaloneBrowser : IBrowser
    {
        private TaskCompletionSource<BrowserResult> _taskCompletionSource;

        /// <summary>
        /// Gets or sets the close page response. This HTML response is shown to the user after redirection is done.
        /// </summary>
        public string closePageResponse { get; set; } = 
            "<html><body><b>DONE!</b><br>(You can close this tab/window now)</body></html>";

        public async Task<BrowserResult> StartAsync(
            string loginUrl, string redirectUrl, CancellationToken cancellationToken = default)
        {
            _taskCompletionSource = new TaskCompletionSource<BrowserResult>();

            cancellationToken.Register(() =>
            {
                _taskCompletionSource?.TrySetCanceled();
            });

            using var httpListener = new HttpListener();
            
            try
            {
                char forwardSlash = '/';
                if (!redirectUrl.EndsWith(forwardSlash))
                {
                    redirectUrl += forwardSlash;
                }
                
                httpListener.Prefixes.Add(redirectUrl);
                httpListener.Start();
                httpListener.BeginGetContext(IncomingHttpRequest, httpListener);

                Application.OpenURL(loginUrl);
                
                return await _taskCompletionSource.Task;
            }
            finally
            {
                httpListener.Stop();
            }
        }

        private void IncomingHttpRequest(IAsyncResult result)
        {
            var httpListener = (HttpListener)result.AsyncState;
            var httpContext = httpListener.EndGetContext(result);
            var httpRequest = httpContext.Request;
            
            // Build a response to send an "ok" back to the browser for the user to see.
            var httpResponse = httpContext.Response;
            var buffer = System.Text.Encoding.UTF8.GetBytes(closePageResponse);

            // Send the output to the client browser.
            httpResponse.ContentLength64 = buffer.Length;
            var output = httpResponse.OutputStream;
            output.Write(buffer, 0, buffer.Length);
            output.Close();

            _taskCompletionSource.SetResult(
                new BrowserResult(BrowserStatus.Success, httpRequest.Url.ToString()));
        }
    }
}

SocketException in Windows build

Hi

I have the following exception in a Windows build (in editor works fine):

SocketException: Solo se permite un uso de cada dirección de socket (protocolo/dirección de red/puerto)
at System.Net.Sockets.Socket.Bind (System.Net.EndPoint localEP) [0x00043] in :0
at System.Net.EndPointListener..ctor (System.Net.HttpListener listener, System.Net.IPAddress addr, System.Int32 port, System.Boolean secure) [0x00047] in :0
at System.Net.EndPointManager.GetEPListener (System.String host, System.Int32 port, System.Net.HttpListener listener, System.Boolean secure) [0x0009d] in :0
at System.Net.EndPointManager.RemovePrefixInternal (System.String prefix, System.Net.HttpListener listener) [0x00040] in :0
at System.Net.EndPointManager.RemoveListener (System.Net.HttpListener listener) [0x00024] in :0
at System.Net.HttpListener.Close (System.Boolean force) [0x00006] in :0
at System.Net.HttpListener.System.IDisposable.Dispose () [0x00009] in :0
at Cdm.Authentication.Browser.StandaloneBrowser.StartAsync (System.String loginUrl, System.String redirectUrl, System.Threading.CancellationToken cancellationToken) [0x0014f] in D:\UNITY PROJECTS2\OAUTH TESTS\OAUTH TESTS\Packages\com.cdm.authentication\Runtime\Browser\StandaloneBrowser.cs:50
at Cdm.Authentication.Browser.CrossPlatformBrowser.StartAsync (System.String loginUrl, System.String redirectUrl, System.Threading.CancellationToken cancellationToken) [0x0009a] in D:\UNITY PROJECTS2\OAUTH TESTS\OAUTH TESTS\Packages\com.cdm.authentication\Runtime\Browser\CrossPlatformBrowser.cs:24
at Cdm.Authentication.OAuth2.AuthenticationSession.AuthenticateAsync (System.Threading.CancellationToken cancellationToken) [0x000df] in D:\UNITY PROJECTS2\OAUTH TESTS\OAUTH TESTS\Packages\com.cdm.authentication\Runtime\OAuth2\AuthenticationSession.cs:65
at MainController.AuthenticateAsync () [0x00092] in D:\UNITY PROJECTS2\OAUTH TESTS\OAUTH TESTS\Assets\UniversalSDK\Demo\Script\MainController.cs:71
UnityEngine.DebugLogHandler:Internal_LogException(Exception, Object)
UnityEngine.DebugLogHandler:LogException(Exception, Object)
UnityEngine.Logger:LogException(Exception, Object)
UnityEngine.Debug:LogException(Exception).

Using: Unity 2021.3.9f1

Thanks!!

PCKE

Hi @cdmdeveloper

I'm adding PCKE to your implementation
https://www.rfc-editor.org/rfc/rfc7636

It seems to work, but for making this work, I have to change several classes (AccessTokenRequest, AuthentitcationSession, AuthorizationCodeFlow, AuthorizationCodeRequest).

These would be the major changes:

        public string GetAuthorizationUrl()
        {
            // Generate new state.
            state = Guid.NewGuid().ToString("D");

            codeVerifier = GenerateRandomDataBase64url(32);
            string codeChallenge = Base64UrlEncodeNoPadding(Sha256Ascii(codeVerifier));
            const string codeChallengeMethod = "S256";

            var parameters = JsonHelper.ToDictionary(new AuthorizationCodeRequest()
            {
                clientId = configuration.clientId,
                redirectUri = configuration.redirectUri,
                scope = configuration.scope,
                state = state,
                codeChallenge = codeChallenge,
                codeChallengeMethod = codeChallengeMethod
            });

            return UrlBuilder.New(authorizationUrl).SetQueryParameters(parameters).ToString();
        }
public virtual async Task<AccessTokenResponse> ExchangeCodeForAccessTokenAsync(string redirectUrl,
            CancellationToken cancellationToken = default)
        {
            var authorizationResponseUri = new Uri(redirectUrl);
            var query = HttpUtility.ParseQueryString(authorizationResponseUri.Query);

            // Is there any error?
            if (JsonHelper.TryGetFromNameValueCollection<AuthorizationCodeRequestError>(query, out var authorizationError))
                throw new AuthorizationCodeRequestException(authorizationError);

            if (!JsonHelper.TryGetFromNameValueCollection<AuthorizationCodeResponse>(query, out var authorizationResponse))
                throw new Exception("Authorization code could not get.");

            // Validate authorization response state.
            if (!string.IsNullOrEmpty(state) && state != authorizationResponse.state)
                throw new SecurityException($"Invalid state got: {authorizationResponse.state}");

            var parameters = JsonHelper.ToDictionary(new AccessTokenRequest()
            {
                code = authorizationResponse.code,
                clientId = configuration.clientId,
                clientSecret = configuration.clientSecret,
                redirectUri = configuration.redirectUri,
                codeVerifier = codeVerifier
            });

            UnityEngine.Debug.Assert(parameters != null);

            accessTokenResponse = 
                await GetAccessTokenInternalAsync(new FormUrlEncodedContent(parameters), cancellationToken);
            return accessTokenResponse;
        }

I'd like to add this functionality in a cleaner way, because I don't want to lose the possibility of using a clean grant code without PCKE

What do you think it could be the cleaner way to do it?

Maybe in the methods GetAuthorizationUrl you can have a virtual GetAuthorizationUrlParameters that can be overwritten in a AuthorizationCodeFlowWithPCKE class

        public string GetAuthorizationUrl()
        {
            // Generate new state.
            state = Guid.NewGuid().ToString("D");

            codeVerifier = GenerateRandomDataBase64url(32);
            string codeChallenge = Base64UrlEncodeNoPadding(Sha256Ascii(codeVerifier));
            const string codeChallengeMethod = "S256";

            var parameters = GetAuthorizationUrlParameters ();


            return UrlBuilder.New(authorizationUrl).SetQueryParameters(parameters).ToString();
        }

protected virtual Dictionary<string, string> GetAuthorizationUrlParameters ()
{
return JsonHelper.ToDictionary(new AuthorizationCodeRequest()
            {
                clientId = configuration.clientId,
                redirectUri = configuration.redirectUri,
                scope = configuration.scope,
                state = state,
                codeChallenge = codeChallenge,
                codeChallengeMethod = codeChallengeMethod
            });
}

Same for ExchangeCodeForAccessTokenAsync.

Thank you

Access ID Token

Hi!
Thanks for a neat package. I'd like to access the user's id_token after authenticating to pass on to another backend, but it isn't included in the AccessTokenResponse. I see that it's a part of the response before it's deserialized, but there is no property for it. Is there a reason for this? Is it possible to access the token somehow? thank you :)

Android Support

Reading the docs I assume there's no android-support yet? Can you clarify and tell about if it's planned and if there's an estimation on when android support will be available if at all?

Exposing userInfoUrl and supporting deserialisation through IUserInfoProvider

For my work to support WebGL as an alternate path to the code that you have written, I need to expose the userInfoUrl to my jslib, and do my own deserialisation because I am unable to use the GetUserInfoAsync method.

Because of this, I want to ask you if you would be OK with adding the userInfoUrl property to IUserInfoProvider, and a method public IUserInfo DeserializeUserInfo(string json). The latter would just do JSONConvert, but it would keep which IUserInfo class to use internally to the *Auth classes; at the moment I need a switch statement to cast objects to the right type.

I can make a PR for you, but I though I'd ask first

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.