I'm new to PLSQL and can't figure out how to restive value from Oracle stored procedures. How can I solve this?
System.Exception: 'Parameter 'result': No size set for variable length data type: String.'
I'm using Iracle 10g
Procedure:
PROCEDURE LoginAuthentication(uid in users.userid%type,pass in users.password%type,result out users.role%type)
is
rl users.role%type;
c number;
begin
select role into rl from users where userid=uid and password=pass and status=1;
select count(*) into c from users where userid=uid and password=pass and status=1;
if(c=1) then
result:=rl;
else
result:='null';
end if;
exception
when no_data_found then
result:='null';
end LoginAuthentication;
Code:
public string Login(String id, string password)
{
using (OracleConnection oCon = new OracleConnection("DATA SOURCE = localhost:1521;USER ID = PROJECTDB;Password=pass"))
{
OracleCommand Cmd = new OracleCommand();
try
{
Cmd.Connection = oCon;
Cmd.CommandText = " user_package.LoginAuthentication";
Cmd.CommandType = CommandType.StoredProcedure;
Cmd.Parameters.Add("uid", OracleType.VarChar).Value = id;
Cmd.Parameters.Add("pass", OracleType.VarChar).Value = password;
Cmd.Parameters.Add("result", OracleType.VarChar).Direction = ParameterDirection.Output;
oCon.Open();
Cmd.ExecuteNonQuery();
string result = Cmd.Parameters["result"].Value.ToString();
return result;
}
catch (Exception ex)
{
throw ex;
}
finally
{
oCon.Close();
}
}
}
Error:
System.Exception: 'Parameter 'result': No size set for variable length data type: String.
CodePudding user response:
The error explains what's wrong - you need to specify the parameter size. In Oracle and SQL Server text parameters require a size. For uid
and pass
the driver can assume the parameter size is equal to the value's size (which can result in truncated values), but with the output parameter, it has no way of knowing what size to use.
You could just pass the size but ....
Cmd.Parameters.Add("result", OracleType.VarChar,20)...
A far bigger problem though is the insecure password code. Passwords should never be stored as clear text, they should be salted and hashed multiple times (1000 at least).
And the word null isn't the same as the null value. Imagine what would happen if someone created a role named "null"
. This isn't nitpicking - systems have been cracked in just this way.
A more secure implementation
All .NET stacks going back to 2002 have a secure authentication and password storage feature. Don't roll your own if you can.
If you absolutely have to (why?) use the Rfc2898DeriveBytes class to hash passwords using a cryptographically strong algorithm.
It's as simple as :
var keygen = new Rfc2898DeriveBytes(pass, salt,1000);
var hash=k1.GetBytes(20);
And using that hash instead of a password. salt
can be any random value. Typically this is stored along with the hash, ensuring every user account has a different salt. Quite often the salt and password are combined and store in a single field. When storing a new password, you could calculate a new salt and hash and store them together eg in a binary(28)
field:
byte[] salt = new byte[8];
using (var rngCsp = new
RNGCryptoServiceProvider())
{
rngCsp.GetBytes(salt);
}
var keygen = new Rfc2898DeriveBytes(pass, salt,1000);
var hash=keygen.GetBytes(20);
var finalHash=new byte[28];
Array.Copy(salt,finalHash,8);
Array.Copy(hash,finalHash,8,20);
...
//Store the hash
Password checking code seldom uses a COUNT(*)
query too. The salt and hash for an account are loaded from the database, a new hash is calculated from user input and the two hashes compared.
The database code could be simplified too. using
is safer than finally
, as finally
can be skipped in some catastrophic exceptions, while using
won't. You could use a library like Dapper as well to eliminate the boilerplate code.
Your code could be simplified to this:
using (var oCon = new OracleConnection("DATA SOURCE = localhost:1521;USER ID = PROJECTDB;Password=pass"))
{
var sql="select hash where userid=:id and status=1";
var accountHash=oCon.SingleOrDefault<byte[]>(sql,new {id=id});
if(accountHash == null)
{
//No such user
}
var salt=accountHash[0..8];
var storedHash=accountHash[8..];
var keygen = new Rfc2898DeriveBytes(pass, salt,1000);
var inputHash=keygen.GetBytes(20);
if(!inputHash.SequenceEqual(storedHash))
{
//Bad password
}
}
It would be even better to use the built-in password storage feature of your stack though.
The query could be modified to return the role as well. Dapper can map query results to objects, so an Account
class or record defined like this:
record Account(byte[] Hash,string Role);
Could be used in the query:
var sql="select Hash,Role where userid=:id and status=1";
var account=oCon.SingleOrDefault<Account>(sql,new {id=id});
CodePudding user response:
Panagiotis Kanavos has explained the error and that you should store the password as a salted hash. You can also improve your database code to eliminate the redundant query to COUNT
the rows as your first query:
select role
into rl
from users
where userid=uid and password=pass and status=1;
Will fail with a NO_DATA_FOUND
exception if there is not a matching row or a TOO_MANY_ROWS
exception if there are duplicate rows so your second query:
select count(*)
into c
from users
where userid=uid and password=pass and status=1;
Will only ever return a value of 1
.
Therefore, your code can be simplified to:
PROCEDURE LoginAuthentication(
uid IN users.userid%type,
pass IN users.password%type,
result OUT users.role%type
)
IS
BEGIN
SELECT role
INTO result
FROM users
WHERE userid = uid
AND password = pass
AND status = 1;
EXCEPTION
WHEN no_data_found THEN
result := 'null';
END LoginAuthentication;
Further, your error-handling block will set the result
to the string literal 'null'
. You probably don't want to use a string literal and just want a NULL
value.
EXCEPTION
WHEN no_data_found THEN
result := NULL;
END LoginAuthentication;