Exemple #1
0
        public async Task SaveTokenAsync()
        {
            // we only support saving a local token (i.e. not updating a remote token)
            if (_tokenStorage != TokenStorageOptions.Authoritative)
            {
                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!");
            }

            // 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 tokens: if a token with this token-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 (_clientId_IsDirty)
            {
                if (_clientId != null)
                {
                    // if there is a client-id assigned to this token, save it.
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"client-id\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(_clientId);
                    iArgument++;
                }
                else
                {
                    // if the client-id has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"client-id\")\n");
                }
                // clear the dirty flag
                _clientId_IsDirty = false;
            }
            //
            if (_accountId_IsDirty)
            {
                if (_accountId != null)
                {
                    // if there is an account-id 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 (_userId_IsDirty)
            {
                if (_userId != null)
                {
                    // if there is a user-id assigned to this token, save it.
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"user-id\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(_userId);
                    iArgument++;
                }
                else
                {
                    // if the user-id has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"user-id\")\n");
                }
                // clear the dirty flag
                _userId_IsDirty = false;
            }
            //
            if (_expirationTime_IsDirty)
            {
                if (_expirationTime != null)
                {
                    // if there is an expiration assigned to this token, save it.
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"expiration-time\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(_expirationTime.Value.ToUnixTimeSeconds().ToString());
                    iArgument++;
                }
                else
                {
                    // if the expiration has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"expiration-time\")\n");
                }
                // clear the dirty flag
                _expirationTime_IsDirty = false;
            }
            if (_refreshTokenId_IsDirty)
            {
                if (_refreshTokenId != null)
                {
                    // if there is a refresh_token assigned to this token, save it.
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"refresh-token-id\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(_refreshTokenId);
                    iArgument++;
                }
                else
                {
                    // if the refresh_token has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"refresh-token-id\")\n");
                }
                // clear the dirty flag
                _refreshTokenId_IsDirty = false;
            }
            // populate the set of scopes
            if (_scopes.IsDirty)
            {
                luaBuilder.Append(objectIsNew ? "" : "redis.call(\"DEL\", KEYS[2])\n");
                foreach (string scope in _scopes)
                {
                    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(scope);
                    iArgument++;
                }

                // clear the dirty flag
                _scopes.IsDirty = false;
            }
            //
            luaBuilder.Append("return 1\n");

            long luaResult = 0;

            for (int iRetry = 0; iRetry < (objectIsNew ? 1000 : 1); iRetry++)
            {
                if (objectIsNew)
                {
                    // generate a 32-byte (192-bit) token_id
                    _id = _authServerId + "-" + (new string(RandomHelper.CreateRandomCharacterSequence_Readable6bit_ForIdentifiers(32)));
                }
                List <string> keys = new List <string>();
                keys.Add(REDIS_PREFIX_OAUTH2TOKEN + REDIS_PREFIX_SEPARATOR + _id);
                keys.Add(REDIS_PREFIX_OAUTH2TOKEN + 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);

                // if we were able to create a key, break out of this loop; otherwise, try generating new keys up to 1000 times.
                if (luaResult == 1)
                {
                    // save our "time-updated" timestamp
                    _timeUpdatedInUnixMicroseconds = newTimeUpdatedInUnixMicroseconds;

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

                        // assign the tokens to its accounts/users now.
                        if (_userId != null)
                        {
                            // if the token belongs to a user (and not more generally to an account), add it to the user's token collection.
                            await _redisClient.SetAddAsync <string, string>(REDIS_PREFIX_USER + REDIS_PREFIX_SEPARATOR + _accountId + REDIS_SLASH + _userId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_OAUTH2TOKENS, new string[] { _id });
                        }
                        else if (_accountId != null)
                        {
                            // if the token belongs to the account (and not to the user), add it to the account's token collection.
                            await _redisClient.SetAddAsync <string, string>(REDIS_PREFIX_ACCOUNT + REDIS_PREFIX_SEPARATOR + _accountId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_OAUTH2TOKENS, 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!");
            }
        }
        public async Task SaveAuthCodeAsync()
        {
            if (_redisClient == null)
            {
                _redisClient = await Singletons.GetRedisClientAsync();
            }

            bool objectIsNew = (_id == null);

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

            // 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 authorization codes: if a token with this authcode-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");
            }
            if (_clientId_IsDirty)
            {
                if (_clientId != null)
                {
                    // if there is a client assigned to this authorization code, save it.
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"client-id\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(_clientId);
                    iArgument++;
                }
                else
                {
                    // if the client-id has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"client-id\")\n");
                }
                // clear the dirty flag
                _clientId_IsDirty = false;
            }
            if (_accountId_IsDirty)
            {
                if (_accountId != null)
                {
                    // if there is an account assigned to this authorization code, 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 (_userId_IsDirty)
            {
                if (_userId != null)
                {
                    // if there is a user assigned to this authorization code, save it.
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"user-id\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(_userId);
                    iArgument++;
                }
                else
                {
                    // if the user-id has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"user-id\")\n");
                }
                // clear the dirty flag
                _userId_IsDirty = false;
            }
            if (_redirectUri_IsDirty)
            {
                if (_redirectUri != null)
                {
                    // if there is a redirect-uri assigned to this authorization code, save it.
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"redirect-uri\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                        "end\n");
                    arguments.Add(_redirectUri);
                    iArgument++;
                }
                else
                {
                    // if the redirect-uri has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"redirect-uri\")\n");
                }
                // clear the dirty flag
                _redirectUri_IsDirty = false;
            }
            // NOTE: when a token is assigned to an authorization code, we re-save the code with the token (in case the same token is reused...in which case we can revoke the already-allocated token)
            if (_tokenId_IsDirty)
            {
                if (_tokenId != null)
                {
                    // if there is a token assigned to this authorization code, save it.
                    luaBuilder.Append(
                        "if redis.call(\"HSET\", KEYS[1], \"token-id\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return 0\n" +
                        "end\n");
                    arguments.Add(_tokenId);
                    iArgument++;
                }
                else
                {
                    // if the token-id has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"token-id\")\n");
                }
                // clear the dirty flag
                _tokenId_IsDirty = false;
            }
            if (_expirationTime_IsDirty)
            {
                if (_expirationTime != null)
                {
                    double expirationMilliseconds = _expirationTime.Value.Subtract(DateTimeOffset.UtcNow).TotalMilliseconds;
                    if (expirationMilliseconds >= 0)
                    {
                        long expirationMillisecondsAsWholeNumber = (long)expirationMilliseconds;

                        // if there is an expiration time assigned to this authorization code, set it.
                        luaBuilder.Append(
                            "if redis.call(\"PEXPIRE\", KEYS[1], ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                            (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                            "  return 0\n" +
                            "end\n");
                        arguments.Add(expirationMillisecondsAsWholeNumber.ToString());
                        iArgument++;
                    }
                }
                else
                {
                    // if the expiration has been removed, delete it.
                    luaBuilder.Append(
                        "if redis.call(\"PERSIST\", KEYS[1]) == 0 then\n" +
                        (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                        "  return 0\n" +
                        "end\n");
                }
                // clear the dirty flag
                _expirationTime_IsDirty = false;
            }
            //
            luaBuilder.Append("return 1\n");

            long luaResult = 0;

            for (int iRetry = 0; iRetry < (objectIsNew ? 1000 : 1); iRetry++)
            {
                if (objectIsNew)
                {
                    // generate a 24-byte (144-bit) token_id
                    _id = _authServerId + "-" + (new string(RandomHelper.CreateRandomCharacterSequence_Readable6bit_ForIdentifiers(24)));
                }
                List <string> keys = new List <string>();
                keys.Add(REDIS_PREFIX_OAUTH2CODE + REDIS_PREFIX_SEPARATOR + _id);
                luaResult = await _redisClient.EvalAsync <string, string, long>(luaBuilder.ToString(), keys.ToArray(), arguments.ToArray()).ConfigureAwait(false);

                //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)
                {
                    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
                {
                    // unknown error
                    throw new Exception("Critical Redis error!");
                }
            }

            if (luaResult < 0)
            {
                throw new Exception("Critical Redis error!");
            }
        }
        public async Task SaveTokenAsync()
        {
            // we only support saving a local token (i.e. not updating a remote token)
            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!");
            }

            // 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 tokens: if a token with this token-id already exists, return 0.
                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 no 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], \"type\", ARGV[" + iArgument.ToString() + "]) == 0 then\n" +
                    (objectIsNew ? "  redis.call(\"DEL\", KEYS[1])\n" : "") +
                    "  return " + RESULT_DATA_CORRUPTION.ToString() + "\n" +
                    "end\n");
                arguments.Add(TOKEN_TYPE_INITIAL_ACCESS_TOKEN);
                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 (_softwareId_IsDirty)
            {
                if (_softwareId != null)
                {
                    // if there is a software-id assigned to this token, save it.
                    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++;
                }
                else
                {
                    // if the software-id has been removed, delete it.
                    luaBuilder.Append("redis.call(\"HDEL\", KEYS[1], \"software-id\")\n");
                }
                // clear the dirty flag
                _softwareId_IsDirty = false;
            }
            //
            if (_accountId_IsDirty)
            {
                if (_accountId != null)
                {
                    // if there is an account-id 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;
            }
            //
            luaBuilder.Append("return 1\n");

            long luaResult = 0;

            for (int iRetry = 0; iRetry < 1000; iRetry++)
            {
                if (objectIsNew)
                {
                    // generate a new 32-byte (192-bit) token_id
                    _id = _loginServerDetails.ToAccountIdServerIdIdentifierString() + "-" + (new string(RandomHelper.CreateRandomCharacterSequence_Readable6bit_ForIdentifiers(32)));
                }
                List <string> keys = new List <string>();
                keys.Add(REDIS_PREFIX_OAUTH2_TOKEN + REDIS_PREFIX_SEPARATOR + _id);
                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 1000 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 token belongs to the entire system (and not to an account), add it to the root token collection.
                            await _redisClient.SetAddAsync <string, string>(REDIS_PREFIX_LOGIN_SERVICE + REDIS_PREFIX_SEPARATOR + REDIS_ASTERISK + REDIS_SLASH + _loginServerDetails.ServerId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_OAUTH2TOKENS, new string[] { _id });

                            await _redisClient.SetAddAsync <string, string>(REDIS_PREFIX_LOGIN_SERVICE + REDIS_PREFIX_SEPARATOR + REDIS_ASTERISK + REDIS_SLASH + _loginServerDetails.ServerId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_OAUTH2TOKENS + REDIS_BRACKET_LEFT + "type" + REDIS_ATSIGN + "initial" + REDIS_BRACKET_RIGHT, new string[] { _id });
                        }
                        else
                        {
                            // if the token belongs to the account (and not to the user), add it to the account's token collection.
                            await _redisClient.SetAddAsync <string, string>(REDIS_PREFIX_LOGIN_SERVICE + REDIS_PREFIX_SEPARATOR + _accountId + REDIS_SLASH + _loginServerDetails.ServerId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_OAUTH2TOKENS, new string[] { _id });

                            await _redisClient.SetAddAsync <string, string>(REDIS_PREFIX_LOGIN_SERVICE + REDIS_PREFIX_SEPARATOR + _accountId + REDIS_SLASH + _loginServerDetails.ServerId + REDIS_SUFFIX_SEPARATOR + REDIS_SUFFIX_OAUTH2TOKENS + REDIS_BRACKET_LEFT + "type" + REDIS_ATSIGN + "initial" + REDIS_BRACKET_RIGHT, 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!");
            }
        }
        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!");
            }
        }