コード例 #1
0
        public static async Task <OAuth2Client> LoadClientAsync(string clientId, LoadClientOptions options)
        {
            if ((options & LoadClientOptions.LocalClients) == LoadClientOptions.LocalClients)
            {
                if (_redisClient == null)
                {
                    _redisClient = await Singletons.GetRedisClientAsync();
                }

                string fullyQualifiedClientKey = REDIS_PREFIX_CLIENT + REDIS_PREFIX_SEPARATOR + clientId;
                bool   localClientExists       = (await _redisClient.ExistsAsync(new string[] { fullyQualifiedClientKey }) > 0);
                if (localClientExists)
                {
                    Dictionary <string, string> clientDictionary = await _redisClient.HashGetAllASync <string, string, string>(fullyQualifiedClientKey);

                    string clientIsCachedAsString = clientDictionary.ContainsKey("cached") ? clientDictionary["cached"] : null;
                    bool   clientIsCached         = (clientIsCachedAsString != null && clientIsCachedAsString != "0");

                    string timeCreatedAsString           = clientDictionary.ContainsKey("time-created") ? clientDictionary["time-created"] : null;
                    Int64? timeCreatedInUnixMicroseconds = null;
                    Int64  timeCreatedAsInt64;
                    if (timeCreatedAsString != null && Int64.TryParse(timeCreatedAsString, out timeCreatedAsInt64))
                    {
                        timeCreatedInUnixMicroseconds = timeCreatedAsInt64;
                    }

                    string timeUpdatedAsString           = clientDictionary.ContainsKey("time-updated") ? clientDictionary["time-updated"] : null;
                    Int64? timeUpdatedInUnixMicroseconds = null;
                    Int64  timeUpdatedAsInt64;
                    if (timeUpdatedAsString != null && Int64.TryParse(timeUpdatedAsString, out timeUpdatedAsInt64))
                    {
                        timeUpdatedInUnixMicroseconds = timeUpdatedAsInt64;
                    }

                    OAuth2Client resultClient = new OAuth2Client();
                    resultClient._id = clientId;
                    ParsingHelper.ServerDetails?loginServerDetails = ParsingHelper.ExtractServerDetailsFromAccountServerIdIdentifier(clientId);
                    if (loginServerDetails == null)
                    {
                        throw new Exception();
                    }
                    resultClient._loginServerDetails = loginServerDetails.Value;
                    //
                    resultClient._accountId = clientDictionary.ContainsKey("account-id") ? clientDictionary["account-id"] : null;
                    //
                    if (clientDictionary.ContainsKey("issued-at"))
                    {
                        long issuedAtAsLong;
                        if (long.TryParse(clientDictionary["issued-at"], out issuedAtAsLong))
                        {
                            resultClient._issuedAt = DateTimeOffset.FromUnixTimeSeconds(issuedAtAsLong);
                        }
                    }
                    //
                    if (clientDictionary.ContainsKey("secret-hash"))
                    {
                        // load the base64-encoded binary hash of the client secret
                        resultClient._secret         = clientDictionary["secret-hash"];
                        resultClient._secretIsHashed = true;
                    }
                    else
                    {
                        resultClient._secret         = null;
                        resultClient._secretIsHashed = false;
                    }
                    //
                    if (resultClient._secret != null)
                    {
                        if (clientDictionary.ContainsKey("expires-at"))
                        {
                            long expiresAtAsLong;
                            if (long.TryParse(clientDictionary["expires-at"], out expiresAtAsLong))
                            {
                                resultClient._expiresAt = DateTimeOffset.FromUnixTimeSeconds(expiresAtAsLong);
                            }
                        }
                    }
                    //
                    resultClient._softwareId = clientDictionary.ContainsKey("software-id") ? clientDictionary["software-id"] : null;
                    //
                    resultClient._softwareVersion = clientDictionary.ContainsKey("software-version") ? clientDictionary["software-version"] : null;
                    //
                    if (!clientDictionary.ContainsKey("token-endpoint-auth-method"))
                    {
                        // this field is required; return null if it is not present.
                        return(null);
                    }
                    resultClient._tokenEndpointAuthMethod = OAuth2Convert.ConvertStringToTokenEndpointAuthMethod(clientDictionary["token-endpoint-auth-method"]).Value;
                    //
                    resultClient._redirectUris = await _redisClient.SetMembersAsync <string, string>(fullyQualifiedClientKey + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_REDIRECT_URIS).ConfigureAwait(false);

                    //
                    List <string> grantTypesAsStrings = await _redisClient.SetMembersAsync <string, string>(fullyQualifiedClientKey + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_GRANT_TYPES).ConfigureAwait(false);

                    if (grantTypesAsStrings != null)
                    {
                        resultClient._grantTypes = new ListWithDirtyFlag <OAuth2GrantType>();
                        foreach (string grantTypeAsString in grantTypesAsStrings)
                        {
                            resultClient._grantTypes.Add(OAuth2Convert.ConvertStringToGrantType(grantTypeAsString).Value);
                        }
                    }
                    //
                    List <string> responseTypesAsStrings = await _redisClient.SetMembersAsync <string, string>(fullyQualifiedClientKey + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_RESPONSE_TYPES).ConfigureAwait(false);

                    if (responseTypesAsStrings != null)
                    {
                        resultClient._responseTypes = new ListWithDirtyFlag <OAuth2ResponseType>();
                        foreach (string responseTypeAsString in responseTypesAsStrings)
                        {
                            resultClient._responseTypes.Add(OAuth2Convert.ConvertStringToResponseType(responseTypeAsString).Value);
                        }
                    }
                    resultClient._scopes = await _redisClient.SetMembersAsync <string, string>(fullyQualifiedClientKey + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_SCOPES).ConfigureAwait(false);

                    //
                    if (clientDictionary.ContainsKey("registration-token-hash"))
                    {
                        // load the base64-encoded binary hash of the client registration token
                        resultClient._registrationToken         = clientDictionary["registration-token-hash"];
                        resultClient._registrationTokenIsHashed = true;
                    }
                    else
                    {
                        resultClient._registrationToken         = null;
                        resultClient._registrationTokenIsHashed = false;
                    }
                    resultClient._isCached = clientIsCached;
                    resultClient._timeCreatedInUnixMicroseconds = timeCreatedInUnixMicroseconds;
                    resultClient._timeUpdatedInUnixMicroseconds = timeUpdatedInUnixMicroseconds;

                    return(resultClient);
                }
            }

            // client could not be found
            return(null);
        }
コード例 #2
0
        public async Task SaveClientAsync()
        {
            // we only support saving a local client (i.e. not updating a remote client)
            if (_isCached)
            {
                throw new InvalidOperationException();
            }

            if (_redisClient == null)
            {
                _redisClient = await Singletons.GetRedisClientAsync();
            }

            bool objectIsNew = (_timeCreatedInUnixMicroseconds == null);

            int RESULT_KEY_CONFLICT       = -1;
            int RESULT_DATA_CORRUPTION    = -2;
            int RESULT_UPDATED_SINCE_LOAD = -3;

            // get current server time
            long newTimeUpdatedInUnixMicroseconds = await _redisClient.TimeAsync();

            if (newTimeUpdatedInUnixMicroseconds < 0)
            {
                throw new Exception("Critical Redis error!");
            }
            if (newTimeUpdatedInUnixMicroseconds < _timeUpdatedInUnixMicroseconds)
            {
                throw new Exception("Critical Redis error!");
            }

            if (objectIsNew)
            {
                // assign clientId, clientSecret, issuedAt time and clientRefreshToken
                // for non-implicit grant types: generate a clientSecret
                if ((_grantTypes.Contains(OAuth2GrantType.AuthorizationCode) && _tokenEndpointAuthMethod != OAuth2TokenEndpointAuthMethod.None) ||
                    _grantTypes.Contains(OAuth2GrantType.ClientCredentials) ||
                    _grantTypes.Contains(OAuth2GrantType.Password))
                {
                    _secret            = new string(RandomHelper.CreateRandomCharacterSequence_Readable6bit_ForIdentifiers(32));
                    _secret_IsDirty    = true;
                    _expiresAt         = null;
                    _expiresAt_IsDirty = true;
                }
                _issuedAt = DateTimeOffset.UtcNow;
                // create client registration token (32-byte == 192-bit)
                /* NOTE: if we ever want to look up the registration token in the #oauth2tokens collections, we will need to start making sure the token is unique-for-server here */
                _registrationToken         = _loginServerDetails.ToAccountIdServerIdIdentifierString() + "-" + (new string(RandomHelper.CreateRandomCharacterSequence_Readable6bit_ForIdentifiers(32)));
                _registrationToken_IsDirty = true;
            }

            // generate Lua script (which we will use to commit all changes--or the new record--in an atomic transaction)
            StringBuilder luaBuilder = new StringBuilder();
            List <string> arguments  = new List <string>();
            int           iArgument  = 1;

            if (objectIsNew)
            {
                // for new clients: if a client with this client-id already exists, return 0...and we will try again.
                luaBuilder.Append(
                    "if redis.call(\"EXISTS\", KEYS[1]) == 1 then\n" +
                    "  return " + RESULT_KEY_CONFLICT.ToString() + "\n" +
                    "end\n");
            }
            else
            {
                // for updated: make sure that the "time-created" timestamp has not changed (i.e. that a new key has not replaced the old key)
                luaBuilder.Append("local time_created = redis.call(\"HGET\", KEYS[1], \"time-created\")\n");
                luaBuilder.Append("if time_created ~= ARGV[" + iArgument.ToString() + "] then\n" +
                                  "  return " + RESULT_KEY_CONFLICT.ToString() + "\n" +
                                  "end\n");
                arguments.Add(_timeCreatedInUnixMicroseconds.ToString());
                iArgument++;

                // for updates: make sure that our old "time-updated" timestamp has not changed
                luaBuilder.Append("local old_time_updated = redis.call(\"HGET\", KEYS[1], \"time-updated\")\n");
                luaBuilder.Append("if old_time_updated ~= ARGV[" + iArgument.ToString() + "] then\n" +
                                  "  return " + RESULT_UPDATED_SINCE_LOAD.ToString() + "\n" +
                                  "end\n");
                arguments.Add(_timeUpdatedInUnixMicroseconds.ToString());
                iArgument++;
            }
            //
            if (objectIsNew)
            {
                luaBuilder.Append(
                    "if redis.call(\"HSET\", KEYS[1], \"time-created\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                    "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                    "end\n");
                arguments.Add(newTimeUpdatedInUnixMicroseconds.ToString());
                iArgument++;
            }
            //
            luaBuilder.Append(
                "if redis.call(\"HSET\", KEYS[1], \"time-updated\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                "end\n");
            arguments.Add(newTimeUpdatedInUnixMicroseconds.ToString());
            iArgument++;
            //
            if (_accountId_IsDirty)
            {
                if (_accountId != null)
                {
                    // if there is an account assigned to this token, save it.
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"account-id\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(_accountId);
                    iArgument++;
                }
                else
                {
                    // if the account-id has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"account-id\")\n");
                }
                // clear the dirty flag
                _accountId_IsDirty = false;
            }
            if (_secret_IsDirty)
            {
                if (_secret != null)
                {
                    // if there is a secret assigned to this client, save it.
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"secret-hash\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(Convert.ToBase64String(HashClientSecret(Encoding.UTF8.GetBytes(_secret))));
                    iArgument++;
                }
                else
                {
                    // if the secret has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"secret-hash\")\n");
                }
                // clear the dirty flag
                _secret_IsDirty = false;
            }
            if (objectIsNew)
            {
                // set the issued-at time
                luaBuilder.Append(
                    "if redis.call(\"HSET\", KEYS[1], \"issued-at\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                    (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                    "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                    "end\n");
                arguments.Add(_issuedAt.Value.ToUnixTimeSeconds().ToString());
                iArgument++;
            }
            // set the expires-at time
            if (_expiresAt_IsDirty)
            {
                if (_expiresAt != null)
                {
                    long expiresAtAsLong = _expiresAt.Value.ToUnixTimeSeconds();

                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"expires-at\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(expiresAtAsLong.ToString());
                    iArgument++;
                }
                else
                {
                    // if the expiration has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"expires-at\")\n");
                }
                // clear the dirty flag
                _expiresAt_IsDirty = false;
            }
            if (_softwareId_IsDirty)
            {
                luaBuilder.Append(
                    "if redis.call(\"HSET\", KEYS[1], \"software-id\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                    (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                    "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                    "end\n");
                arguments.Add(_softwareId);
                iArgument++;

                // clear the dirty flag
                _softwareId_IsDirty = false;
            }
            if (_softwareVersion_IsDirty)
            {
                if (_softwareVersion != null)
                {
                    // set the softwareVersion
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"software-version\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(_softwareVersion);
                    iArgument++;
                }
                else
                {
                    // if the software-version has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"software-version\")\n");
                }
                // clear the dirty flag
                _softwareVersion_IsDirty = false;
            }
            // set the tokenEndpointAuthMethod
            if (_tokenEndpointAuthMethod_IsDirty)
            {
                luaBuilder.Append(
                    "if redis.call(\"HSET\", KEYS[1], \"token-endpoint-auth-method\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                    (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                    "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                    "end\n");
                arguments.Add(OAuth2Convert.ConvertTokenEndpointAuthMethodToString(_tokenEndpointAuthMethod));
                iArgument++;

                // clear the dirty flag
                _tokenEndpointAuthMethod_IsDirty = false;
            }
            //populate the set of redirect-uris
            if (_redirectUris.IsDirty)
            {
                luaBuilder.Append(objectIsNew ? "" : "redis.call(\"DEL\", KEYS[2])\n");
                foreach (string redirectUri in _redirectUris)
                {
                    luaBuilder.Append(
                        "if redis.call(\"SADD\", KEYS[2], ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[2])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(redirectUri);
                    iArgument++;
                }

                // clear the dirty flag
                _redirectUris.IsDirty = false;
            }
            // populate the set of grant-types
            if (_grantTypes.IsDirty)
            {
                luaBuilder.Append(objectIsNew ? "" : "redis.call(\"DEL\", KEYS[3])\n");
                foreach (OAuth2GrantType grantType in _grantTypes)
                {
                    luaBuilder.Append(
                        "if redis.call(\"SADD\", KEYS[3], ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[2])\n" : "") +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[3])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(OAuth2Convert.ConvertGrantTypeToString(grantType));
                    iArgument++;
                }

                // clear the dirty flag
                _grantTypes.IsDirty = false;
            }
            // populate the set of response-types
            if (_responseTypes.IsDirty)
            {
                luaBuilder.Append(objectIsNew ? "" : "redis.call(\"DEL\", KEYS[4])\n");
                foreach (OAuth2ResponseType responseType in _responseTypes)
                {
                    luaBuilder.Append(
                        "if redis.call(\"SADD\", KEYS[4], ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[2])\n" : "") +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[3])\n" : "") +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[4])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(OAuth2Convert.ConvertResponseTypeToString(responseType));
                    iArgument++;
                }

                // clear the dirty flag
                _responseTypes.IsDirty = false;
            }
            // populate the set of scopes
            if (_scopes.IsDirty)
            {
                luaBuilder.Append(objectIsNew ? "" : "redis.call(\"DEL\", KEYS[5])\n");
                foreach (string scope in _scopes)
                {
                    luaBuilder.Append(
                        "if redis.call(\"SADD\", KEYS[5], ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[2])\n" : "") +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[3])\n" : "") +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[4])\n" : "") +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[5])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(scope);
                    iArgument++;
                }

                // clear the dirty flag
                _scopes.IsDirty = false;
            }
            //
            if (_registrationToken_IsDirty)
            {
                if (_registrationToken != null)
                {
                    // if there is a registration token assigned to this client, save it.
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"registration-token-hash\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(Convert.ToBase64String(HashClientRegistrationToken(Encoding.UTF8.GetBytes(_registrationToken))));
                    iArgument++;
                }
                else
                {
                    // if the registration token has been removed, delete it.
                    // NOTE: this operation is technically supported by spec--but removing the ability for a client to manage a token (usually because of third-party meddling) can have some unfortunate consequences as well
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"registration-token-hash\")\n");
                }
                // clear the dirty flag
                _registrationToken_IsDirty = false;
            }
            //
            luaBuilder.Append("return 1\n");

            long luaResult = 0;

            for (int iRetry = 0; iRetry < 1000; iRetry++)
            {
                if (objectIsNew)
                {
                    // generate a 32-byte (192-bit) client_id
                    _id = _loginServerDetails.ToAccountIdServerIdIdentifierString() + "-" + (new string(RandomHelper.CreateRandomCharacterSequence_Readable6bit_ForIdentifiers(32)));
                }
                List <string> keys = new List <string>();
                keys.Add(REDIS_PREFIX_CLIENT + REDIS_PREFIX_SEPARATOR + _id);
                keys.Add(REDIS_PREFIX_CLIENT + REDIS_PREFIX_SEPARATOR + _id + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_REDIRECT_URIS);
                keys.Add(REDIS_PREFIX_CLIENT + REDIS_PREFIX_SEPARATOR + _id + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_GRANT_TYPES);
                keys.Add(REDIS_PREFIX_CLIENT + REDIS_PREFIX_SEPARATOR + _id + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_RESPONSE_TYPES);
                keys.Add(REDIS_PREFIX_CLIENT + REDIS_PREFIX_SEPARATOR + _id + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_SCOPES);
                luaResult = await _redisClient.EvalAsync <string, string, long>(luaBuilder.ToString(), keys.ToArray(), arguments.ToArray()).ConfigureAwait(false);

                // NOTE: the result will contain a negative integer (error) or one (success)
                // if we were able to create a key, break out of this loop; otherwise, try generating new keys up to ten times.
                if (luaResult == 1)
                {
                    // save our "time-updated" timestamp
                    _timeUpdatedInUnixMicroseconds = newTimeUpdatedInUnixMicroseconds;

                    if (objectIsNew)
                    {
                        // save our "time-created" timestamp
                        _timeCreatedInUnixMicroseconds = newTimeUpdatedInUnixMicroseconds;

                        if (_accountId == null)
                        {
                            // if the client belongs to the entire system (and not to an account), add it to the root client collection.
                            await _redisClient.SetAddAsync <string, string>(REDIS_PREFIX_LOGIN_SERVICE + REDIS_PREFIX_SEPARATOR + REDIS_ASTERISK + REDIS_SLASH + _loginServerDetails.ServerId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_OAUTH2CLIENTS, new string[] { _id });
                        }
                        else
                        {
                            // if the client belongs to the account (and not to the user), add it to the account's client collection.
                            await _redisClient.SetAddAsync <string, string>(REDIS_PREFIX_LOGIN_SERVICE + REDIS_PREFIX_SEPARATOR + _accountId + REDIS_SLASH + _loginServerDetails.ServerId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_OAUTH2CLIENTS, new string[] { _id });
                        }
                    }
                    break;
                }
                else if (luaResult == RESULT_KEY_CONFLICT)
                {
                    // key name conflict; try again
                }
                else if (luaResult == RESULT_DATA_CORRUPTION)
                {
                    // data corruption
                    throw new Exception("Critical Redis error!");
                }
                else if (luaResult == RESULT_UPDATED_SINCE_LOAD)
                {
                    // token was updated since we loaded it; we need to reload the token, make the changes again, and then attempt to save it again
                    throw new Exception("Critical Redis error!");
                }
                else
                {
                    // unknown error
                    throw new Exception("Critical Redis error!");
                }
            }

            if (luaResult < 0)
            {
                throw new Exception("Critical Redis error!");
            }
        }