feat: Authentication Framework (#1057)

* Component-based Authentication

* Capitalized IsAuthenticated

* Added isAuthenticated to NetworkConnection

* Removed activeAuthenticator as unnecessary

* Removed unnecessary using

* Added more comments

* Documentation

* Added cs to code blocks in doc

* fixed typo in doc

* Doc improvements

* Fixed another typo in doc

* Removed HideInInspector

* Updated doc and image

* Fixed comment

* Added inspector header and tooltips

* Fixed typo

* Add AuthenticationData object

* Add a bullet point in the doc about AuthenticationData

* Updated screenshot image

* Added HelpURL attribute

* Added Initializers for both Server and Client

* Fixed doc grammar and phrasing

* Forgot to add the ClientInitialize in StartHost

* Updated doc with info about the initializers

* Changed initializers from bool to void.

* Eliminated the abstract model and renamed to NetworkAuthenticator and made all methods virtual

* Fixed comment

* Fixed typo

* Doc cleanup

* Doc Cleanup

* authenticator RemoveAllListeners in StopServer and StopClient

* Update Assets/Mirror/Runtime/NetworkManager.cs

Co-Authored-By: vis2k <info@noobtuts.com>

* Changes requested by Vis

* reverted conflicting change

* Revert "reverted conflicting change"

This reverts commit f65870e073.

* UnityEditor.Undo.RecordObject

* made the name camelCase

* Added internal methods and On prefix to methods

* Reverted this change so it can be done in a separate PR

* Moved authenticator calls to after runInBackground

* Add built-in timeout feature

* Changed UnityEditor.Undo.RecordObject to use gameobject

* Convert to Abstract, add Basic Authenticator, update docs.

* Removed timeout, against my better judgement.

* Removed the rest of timeout, still against my better judgement

* Fixed event listener mappings

* Renamed and consolidated methods

* updated doc and image

* made OnClientAuthenticate and OnServerAuthenticate abstract

* Updated Debug log msgs

* changed to authenticator != null

* Renamed to NetworkAuthenticator
This commit is contained in:
MrGadget 2019-09-17 04:41:04 -04:00 committed by vis2k
parent e67035e971
commit 56bcb02c15
11 changed files with 366 additions and 49 deletions

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1b2f9d254154cd942ba40b06b869b8f3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,116 @@
using UnityEngine;
using System.Collections;
namespace Mirror.Authenticators
{
[AddComponentMenu("Network/Authenticators/BasicAuthenticator")]
public class BasicAuthenticator : NetworkAuthenticator
{
[Header("Custom Properties")]
// set these in the inspector
public string username;
public string password;
public class AuthRequestMessage : MessageBase
{
// use whatever credentials make sense for your game
// for example, you might want to pass the accessToken if using oauth
public string authUsername;
public string authPassword;
}
public class AuthResponseMessage : MessageBase
{
public byte code;
public string message;
}
public override void OnStartServer()
{
// register a handler for the authentication request we expect from client
NetworkServer.RegisterHandler<AuthRequestMessage>(OnAuthRequestMessage);
}
public override void OnStartClient()
{
// register a handler for the authentication response we expect from server
NetworkClient.RegisterHandler<AuthResponseMessage>(OnAuthResponseMessage);
}
public override void OnServerAuthenticate(NetworkConnection conn)
{
// do nothing...wait for AuthRequestMessage from client
}
public override void OnClientAuthenticate(NetworkConnection conn)
{
AuthRequestMessage authRequestMessage = new AuthRequestMessage
{
authUsername = username,
authPassword = password
};
NetworkClient.Send(authRequestMessage);
}
public void OnAuthRequestMessage(NetworkConnection conn, AuthRequestMessage msg)
{
Debug.LogFormat("Authentication Request: {0} {1}", msg.authUsername, msg.authPassword);
// check the credentials by calling your web server, database table, playfab api, or any method appropriate.
if (msg.authUsername == username && msg.authPassword == password)
{
// create and send msg to client so it knows to proceed
AuthResponseMessage authResponseMessage = new AuthResponseMessage
{
code = 100,
message = "Success"
};
NetworkServer.SendToClient(conn.connectionId, authResponseMessage);
// Invoke the event to complete a successful authentication
base.OnServerAuthenticated.Invoke(conn);
}
else
{
// create and send msg to client so it knows to disconnect
AuthResponseMessage authResponseMessage = new AuthResponseMessage
{
code = 200,
message = "Invalid Credentials"
};
NetworkServer.SendToClient(conn.connectionId, authResponseMessage);
// must set NetworkConnection isAuthenticated = false
conn.isAuthenticated = false;
// disconnect the client after 1 second so that response message gets delivered
Invoke(nameof(conn.Disconnect), 1);
}
}
public void OnAuthResponseMessage(NetworkConnection conn, AuthResponseMessage msg)
{
if (msg.code == 100)
{
Debug.LogFormat("Authentication Response: {0}", msg.message);
// Invoke the event to complete a successful authentication
base.OnClientAuthenticated.Invoke(conn);
}
else
{
Debug.LogErrorFormat("Authentication Response: {0}", msg.message);
// Set this on the client for local reference
conn.isAuthenticated = false;
// disconnect the client
conn.Disconnect();
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 28496b776660156428f00cf78289c1ec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,14 @@
{
"name": "Mirror.Authenticators",
"references": [
"Mirror"
],
"optionalUnityReferences": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": []
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e720aa64e3f58fb4880566a322584340
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,90 @@
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
namespace Mirror
{
/// <summary>
/// Unity Event for the NetworkConnection
/// </summary>
[Serializable] public class UnityEventNetworkConnection : UnityEvent<NetworkConnection> { }
/// <summary>
/// Base class for implementing component-based authentication during the Connect phase
/// </summary>
[HelpURL("https://mirror-networking.com/xmldocs/articles/Concepts/Authentication.html")]
public abstract class NetworkAuthenticator : MonoBehaviour
{
[Header("Event Listeners (optional)")]
/// <summary>
/// Notify subscribers on the server when a client is authenticated
/// </summary>
[Tooltip("Mirror has an internal subscriber to this event. You can add your own here.")]
public UnityEventNetworkConnection OnServerAuthenticated = new UnityEventNetworkConnection();
/// <summary>
/// Notify subscribers on the client when the client is authenticated
/// </summary>
[Tooltip("Mirror has an internal subscriber to this event. You can add your own here.")]
public UnityEventNetworkConnection OnClientAuthenticated = new UnityEventNetworkConnection();
#region server
/// <summary>
/// Called on server from StartServer to initialize the Authenticator
/// <para>Server message handlers should be registered in this method.</para>
/// </summary>
public abstract void OnStartServer();
/// <summary>
/// Called on client from StartClient to initialize the Authenticator
/// <para>Client message handlers should be registered in this method.</para>
/// </summary>
public abstract void OnStartClient();
// This will get more code in the near future
internal void OnServerAuthenticateInternal(NetworkConnection conn)
{
OnServerAuthenticate(conn);
}
/// <summary>
/// Called on server from OnServerAuthenticateInternal when a client needs to authenticate
/// </summary>
/// <param name="conn">Connection to client.</param>
public abstract void OnServerAuthenticate(NetworkConnection conn);
#endregion
#region client
// This will get more code in the near future
internal void OnClientAuthenticateInternal(NetworkConnection conn)
{
OnClientAuthenticate(conn);
}
/// <summary>
/// Called on client from OnClientAuthenticateInternal when a client needs to authenticate
/// </summary>
/// <param name="conn">Connection of the client.</param>
public abstract void OnClientAuthenticate(NetworkConnection conn);
#endregion
void OnValidate()
{
#if UNITY_EDITOR
// automatically assign NetworkManager field if we add this to NetworkManager
NetworkManager manager = GetComponent<NetworkManager>();
if (manager != null && manager.authenticator == null)
{
manager.authenticator = this;
UnityEditor.Undo.RecordObject(gameObject, "Assigned NetworkManager authenticator");
}
#endif
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 407fc95d4a8257f448799f26cdde0c2a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -32,6 +32,17 @@ public class NetworkConnection : IDisposable
/// </remarks>
public int connectionId = -1;
/// <summary>
/// Flag that indicates the client has been authenticated.
/// </summary>
public bool isAuthenticated;
/// <summary>
/// General purpose object to hold authentication data, character selection, tokens, etc.
/// associated with the connection for reference after Authentication completes.
/// </summary>
public object authenticationData;
/// <summary>
/// Flag that tells if the connection has been marked as "ready" by a client calling ClientScene.Ready().
/// <para>This property is read-only. It is set by the system on the client when ClientScene.Ready() is called, and set by the system on the server when a ready message is received from a client.</para>

View File

@ -92,6 +92,10 @@ public class NetworkManager : MonoBehaviour
[FormerlySerializedAs("m_MaxConnections")]
public int maxConnections = 4;
[Header("Authentication")]
public NetworkAuthenticator authenticator;
[Header("Spawn Info")]
/// <summary>
@ -394,6 +398,12 @@ public bool StartServer()
if (runInBackground)
Application.runInBackground = true;
if (authenticator != null)
{
authenticator.OnStartServer();
authenticator.OnServerAuthenticated.AddListener(OnServerAuthenticated);
}
ConfigureServerFrameRate();
if (!NetworkServer.Listen(maxConnections))
@ -461,6 +471,12 @@ public void StartClient()
{
InitializeSingleton();
if (authenticator != null)
{
authenticator.OnStartClient();
authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated);
}
if (runInBackground)
Application.runInBackground = true;
@ -497,6 +513,13 @@ public virtual void StartHost()
void ConnectLocalClient()
{
if (LogFilter.Debug) Debug.Log("NetworkManager StartHost");
if (authenticator != null)
{
authenticator.OnStartClient();
authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated);
}
networkAddress = "localhost";
NetworkServer.ActivateLocalClientScene();
NetworkClient.ConnectLocalServer();
@ -522,6 +545,9 @@ public void StopServer()
if (!NetworkServer.active)
return;
if (authenticator != null)
authenticator.OnServerAuthenticated.RemoveListener(OnServerAuthenticated);
OnStopServer();
if (LogFilter.Debug) Debug.Log("NetworkManager StopServer");
@ -541,6 +567,9 @@ public void StopServer()
/// </summary>
public void StopClient()
{
if (authenticator != null)
authenticator.OnClientAuthenticated.RemoveListener(OnClientAuthenticated);
OnStopClient();
if (LogFilter.Debug) Debug.Log("NetworkManager StopClient");
@ -752,6 +781,27 @@ void OnServerConnectInternal(NetworkConnection conn, ConnectMessage connectMsg)
{
if (LogFilter.Debug) Debug.Log("NetworkManager.OnServerConnectInternal");
if (authenticator != null)
{
// we have an authenticator - let it handle authentication
authenticator.OnServerAuthenticateInternal(conn);
}
else
{
// authenticate immediately
OnServerAuthenticated(conn);
}
}
// called after successful authentication
void OnServerAuthenticated(NetworkConnection conn)
{
if (LogFilter.Debug) Debug.Log("NetworkManager.OnServerAuthenticated");
// set connection to authenticated
conn.isAuthenticated = true;
// proceed with the login handshake by calling OnServerConnect
if (networkSceneName != "" && networkSceneName != offlineScene)
{
SceneMessage msg = new SceneMessage() { sceneName = networkSceneName };
@ -823,6 +873,27 @@ void OnClientConnectInternal(NetworkConnection conn, ConnectMessage message)
{
if (LogFilter.Debug) Debug.Log("NetworkManager.OnClientConnectInternal");
if (authenticator != null)
{
// we have an authenticator - let it handle authentication
authenticator.OnClientAuthenticateInternal(conn);
}
else
{
// authenticate immediately
OnClientAuthenticated(conn);
}
}
// called after successful authentication
void OnClientAuthenticated(NetworkConnection conn)
{
if (LogFilter.Debug) Debug.Log("NetworkManager.OnClientAuthenticated");
// set connection to authenticated
conn.isAuthenticated = true;
// proceed with the login handshake by calling OnClientConnect
string loadedSceneName = SceneManager.GetActiveScene().name;
if (string.IsNullOrEmpty(onlineScene) || onlineScene == offlineScene || loadedSceneName == onlineScene)
{

View File

@ -16,67 +16,45 @@ When you have a multiplayer game, often you need to store information about your
- Use a web service in your website
Trying to write a comprehensive authentication framework that cover all these is very complex. There is no one size fit all, and we would quickly end up with bloated code.
Mirror includes an `Authenticator` abstract class that allows you to implement any authentication scheme you need.
Instead, **Mirror does not perform authentication**, but we provide hooks you can use to implement any of these.
## Encryption Warning
Here is an example of how to implement simple username/password authentication:
By default Mirror uses Telepathy, which is not encrypted, so if you want to do authentication through Mirror, we highly recommend you use a transport that supports encryption.
1. Select your `NetworkManager` game object in the unity editor.
## Custom Authenticators
2. In the inspector, under `Spawn Info`, disable `Auto Create Player`
To make your own custom Authenticator, you can just create a new script in your project (not in the Mirror folders) that inherits from `Authenticator` and override the methods as needed.
3. Call `AddPlayer` in your client to pass the credentials.
- When a client is authenticated to your satisfaction, you simply call `base.OnServerAuthenticated.Invoke(conn)` and `base.OnClientAuthenticated.Invoke(conn)` on the server and client, respectively. Mirror is listening for these events to proceed with the connection sequence.
4. Override the `OnServerAddPlayer` method and validate the user's credential.
- In the inspector you can optionally subscribe your own methods to the OnServerAuthenticated and OnClientAuthenticated events.
For example this would be part of your `NetworkManager` class:
Here are some tips for custom Authenticators:
```cs
class MyGameNetworkManager : NetworkManager
{
class CredentialsMessage : MessageBase
{
// use whatever credentials make sense for your game
// for example, you might want to pass the accessToken if using oauth
public string username;
public string password;
}
- `OnStartServer` and `OnStartClient` are the appropriate methods to register server and client messages and their handlers. They're called from StartServer/StartHost, and StartClient, respectively.
// this gets called on the client after it has connected to the server
public override void OnClientConnect(NetworkConnection conn)
{
base.OnClientConnect(conn);
- Send a message to the client if authentication fails, especially if there's some issue they can resolve.
CredentialsMessage msg = new CredentialsMessage()
{
// perhaps get the username and password from textboxes instead
username = "Joe",
password = "Gaba Gaba"
};
- Call the `Disconnect()` method of the `NetworkConnection` on the server and client when authentication fails. If you want to give the user a few tries to get their credentials right, you certainly can, but Mirror will not do the disconnect for you.
ClientScene.AddPlayer(conn, MessagePacker.Pack(msg));
}
- Remember to put a small delay on the Disconnect call on the server if you send a failure message so that it has a chance to be delivered before the connection is dropped.
// this gets called in your server when the client requests to add a player.
public override void OnServerAddPlayer(NetworkConnection conn, AddPlayerMessage extraMessage)
{
CredentialsMessage msg = MessagePacker.Unpack<CredentialsMessage>(extraMessage.value);
- `NetworkConnection` has an `AuthenticationData` object where you can drop a class instance of any data you need to persist on the server related to the authentication, such as account id's, tokens, character selection, etc.
// check the credentials by calling your web server, database table, playfab api, or any method appropriate.
if (msg.username == "Joe" && msg.password == "Gaba Gaba")
{
// proceed to regular add player
base.OnServerAddPlayer(conn, extraMessage);
}
else
{
conn.Disconnect();
}
}
}
```
Now that you have the foundation of a custom Authenticator component, the rest is up to you. You can exchange any number of custom messages between the server and client as necessary to complete your authentication process before approving the client.
## Warning
## Basic Authenticator
Mirror includes a Basic Authenticator in the Mirror / Authenticators folder which just uses a simple username and password.
- Drag the Basic Authenticator script to the inspector of the object in your scene that has Network Manager
- The Basic Authenticator component will automatically be assigned to the Authenticator field in Network Manager
When you're done, it should look like this:
![Inspector showing Basic Authentication component](BasicAuthentication.PNG)
> **Note:** You don't need to assign anything to the event lists unless you want to subscribe to the events in your own code for your own purposes. Mirror has internal listeners for both events.
By default Mirror uses Telepathy, which is not encrypted. The above code sample works, but if you want to do authentication through Mirror, we highly recommend you use a transport that supports encryption.

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB