diff --git a/Assets/Mirror/Authenticators.meta b/Assets/Mirror/Authenticators.meta new file mode 100644 index 000000000..644f4ecc9 --- /dev/null +++ b/Assets/Mirror/Authenticators.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1b2f9d254154cd942ba40b06b869b8f3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Authenticators/BasicAuthenticator.cs b/Assets/Mirror/Authenticators/BasicAuthenticator.cs new file mode 100644 index 000000000..666301119 --- /dev/null +++ b/Assets/Mirror/Authenticators/BasicAuthenticator.cs @@ -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(OnAuthRequestMessage); + } + + public override void OnStartClient() + { + // register a handler for the authentication response we expect from server + NetworkClient.RegisterHandler(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(); + } + } + } +} diff --git a/Assets/Mirror/Authenticators/BasicAuthenticator.cs.meta b/Assets/Mirror/Authenticators/BasicAuthenticator.cs.meta new file mode 100644 index 000000000..5984986b3 --- /dev/null +++ b/Assets/Mirror/Authenticators/BasicAuthenticator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 28496b776660156428f00cf78289c1ec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef b/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef new file mode 100644 index 000000000..16cdfbc2f --- /dev/null +++ b/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef @@ -0,0 +1,14 @@ +{ + "name": "Mirror.Authenticators", + "references": [ + "Mirror" + ], + "optionalUnityReferences": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [] +} \ No newline at end of file diff --git a/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef.meta b/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef.meta new file mode 100644 index 000000000..273170109 --- /dev/null +++ b/Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e720aa64e3f58fb4880566a322584340 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkAuthenticator.cs b/Assets/Mirror/Runtime/NetworkAuthenticator.cs new file mode 100644 index 000000000..96fafadf8 --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkAuthenticator.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections; +using UnityEngine; +using UnityEngine.Events; + +namespace Mirror +{ + /// + /// Unity Event for the NetworkConnection + /// + [Serializable] public class UnityEventNetworkConnection : UnityEvent { } + + /// + /// Base class for implementing component-based authentication during the Connect phase + /// + [HelpURL("https://mirror-networking.com/xmldocs/articles/Concepts/Authentication.html")] + public abstract class NetworkAuthenticator : MonoBehaviour + { + [Header("Event Listeners (optional)")] + + /// + /// Notify subscribers on the server when a client is authenticated + /// + [Tooltip("Mirror has an internal subscriber to this event. You can add your own here.")] + public UnityEventNetworkConnection OnServerAuthenticated = new UnityEventNetworkConnection(); + + /// + /// Notify subscribers on the client when the client is authenticated + /// + [Tooltip("Mirror has an internal subscriber to this event. You can add your own here.")] + public UnityEventNetworkConnection OnClientAuthenticated = new UnityEventNetworkConnection(); + + #region server + + /// + /// Called on server from StartServer to initialize the Authenticator + /// Server message handlers should be registered in this method. + /// + public abstract void OnStartServer(); + + /// + /// Called on client from StartClient to initialize the Authenticator + /// Client message handlers should be registered in this method. + /// + public abstract void OnStartClient(); + + // This will get more code in the near future + internal void OnServerAuthenticateInternal(NetworkConnection conn) + { + OnServerAuthenticate(conn); + } + + /// + /// Called on server from OnServerAuthenticateInternal when a client needs to authenticate + /// + /// Connection to client. + 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); + } + + /// + /// Called on client from OnClientAuthenticateInternal when a client needs to authenticate + /// + /// Connection of the client. + 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(); + if (manager != null && manager.authenticator == null) + { + manager.authenticator = this; + UnityEditor.Undo.RecordObject(gameObject, "Assigned NetworkManager authenticator"); + } +#endif + } + } +} diff --git a/Assets/Mirror/Runtime/NetworkAuthenticator.cs.meta b/Assets/Mirror/Runtime/NetworkAuthenticator.cs.meta new file mode 100644 index 000000000..c6bbca44c --- /dev/null +++ b/Assets/Mirror/Runtime/NetworkAuthenticator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 407fc95d4a8257f448799f26cdde0c2a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Runtime/NetworkConnection.cs b/Assets/Mirror/Runtime/NetworkConnection.cs index 98c078e60..2b24ace6a 100644 --- a/Assets/Mirror/Runtime/NetworkConnection.cs +++ b/Assets/Mirror/Runtime/NetworkConnection.cs @@ -32,6 +32,17 @@ public class NetworkConnection : IDisposable /// public int connectionId = -1; + /// + /// Flag that indicates the client has been authenticated. + /// + public bool isAuthenticated; + + /// + /// General purpose object to hold authentication data, character selection, tokens, etc. + /// associated with the connection for reference after Authentication completes. + /// + public object authenticationData; + /// /// Flag that tells if the connection has been marked as "ready" by a client calling ClientScene.Ready(). /// 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. diff --git a/Assets/Mirror/Runtime/NetworkManager.cs b/Assets/Mirror/Runtime/NetworkManager.cs index d37c81b70..9d6e9f3b6 100644 --- a/Assets/Mirror/Runtime/NetworkManager.cs +++ b/Assets/Mirror/Runtime/NetworkManager.cs @@ -92,6 +92,10 @@ public class NetworkManager : MonoBehaviour [FormerlySerializedAs("m_MaxConnections")] public int maxConnections = 4; + [Header("Authentication")] + + public NetworkAuthenticator authenticator; + [Header("Spawn Info")] /// @@ -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() /// 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) { diff --git a/doc/articles/Concepts/Authentication.md b/doc/articles/Concepts/Authentication.md index bfa150b3e..bda9a7589 100644 --- a/doc/articles/Concepts/Authentication.md +++ b/doc/articles/Concepts/Authentication.md @@ -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(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. diff --git a/doc/articles/Concepts/BasicAuthentication.PNG b/doc/articles/Concepts/BasicAuthentication.PNG new file mode 100644 index 000000000..735912cdc Binary files /dev/null and b/doc/articles/Concepts/BasicAuthentication.PNG differ