I wanted to implement custom authentication in my oracle apex application. So for that I wrote some pl/sql codes to create a package.
create or replace package body user_tools_pak
as
function login_auth(field_userid in varchar2,field_password in varchar2)
return boolean is
begin
select user_id
from USER_TABLE
where user_id = field_userid
and password = field_password;
apex_util.set_session_state(p_name => 'SESSION_USERID', p_value => user_id);
return true;
exception
when no_data_found then
return false;
end login_auth;
end user_tools_pak;
But when I run it an error occurs.
Is there anything wrong with my code or am I missing something?
The function should accept p_username and p_password as parameters and return boolean. This can be found in the help section of the authentication function name
Specify the name of the function that will verify the user's username
and password, after they were entered on a login page. If you enter
nothing, you allow any username/password to succeed. The function
itself can be defined in the authentication's 'PL/SQL Code' textarea,
within a package or as a stored function.
This function must return a boolean to the login procedure that calls
it. It has 2 input parameters 'p_username' and 'p_password' that can
be used to access the values an end user entered on the login page.
Examples
Enter the following code in the 'PL/SQL Code' textarea
function my_authentication (
p_username in varchar2,
p_password in varchar2 )
return boolean
is
l_user my_users.user_name%type := upper(p_username);
l_id my_users.id%type;
l_hash my_users.password_hash%type;
begin
begin
select id , password_hash
into l_id, l_hash
from my_users
where user_name = l_user;
exception when no_data_found then
l_hash := '-invalid-';
end;
return l_hash = rawtohex(sys.dbms_crypto.hash (
sys.utl_raw.cast_to_raw (
p_password||l_id||l_user ),
sys.dbms_crypto.hash_sh512 ));
end;
and my_authentication as 'Authentication Function'.
I am fairly new to PL/SQL and one of the doubts I have is to create and declare functions.
Specifically this one:
Create a function that creates a new user:
Use a sequence togive the new User a new ID
Pass the name, address, etc... as IN arguments
Return as OUT arguments the ID created and a O_ERROR_MESSAGE
Function returns TRUE if the user is added, otherwise returns FALSE
Handle exceptions
Create a PL/SQL block and test the created function
This is my code so far:
CREATE OR REPLACE FUNCTION DSB_ADD_NEW_USERS (I_NAME IN VARCHAR2,
I_ADDRESS IN VARCHAR2,
I_BIRTHDATE IN DATE,
I_COUNTRY IN VARCHAR2)
RETURN NUMBER IS
O_ERROR_MESSAGE EXCEPTION;
CURRENT_USER NUMBER;
BEGIN
DSB_NB_SEQ_USER_ID.NEXTVAL;
SELECT COUNT(USER_ID) INTO CURRENT_USER
FROM DSB_NB_USERS;
WHILE CURRENT_USER != 0
LOOP
DSB_NB_SEQ_USER_ID.NEXTVAL;
SELECT COUNT(USER_ID) INTO CURRENT_USER
FROM DSB_NB_USERS;
END LOOP;
INSERT INTO DSB_NB_USERS (USER_ID, NAME, ADDRESS, BIRTHDATE, COUNTRY_ID_FK) VALUES (CURRENT_USER, I_NAME, I_ADDRESS, TO_DATE('I_BIRTHDATE', 'DD/MM/YYYY'), I_COUNTRY);
RETURN CURRET_USER;
EXCEPTION
WHEN O_ERROR_MESSAGE THEN
RETURN NULL;
WHEN OTHERS THEN
RETURN NULL;
END;
DECLARE
I_NEW_USER NUMBER;
BEGIN
I_NEW_USER := DSB_ADD_NEW_USERS(I_NAME => 'Arnaldo Amaral',
I_ADDRESS => 'Rua da Agra',
I_BIRTHDATE => '03/05/1959',
I_COUNTRY => 'PT');
END;
Am i too far from the truth?
I know there's still a lot to add.
Thank you for the help!!!
It seems to me you are asking for a lifeline, that needs more than just a code response. First off look at the answer by
#Tejash. His solution is a far cry from what you have, and is correct for your function definition (although that is itself incorrect given your listed requirements.) Also, #EdStevens is correct that is is a very poor use of a function. However a function is your assignment's requirement.
So lets dissect your code and see how it satisfies each of your requirements. (I cannot stand all caps, so I'm lower casing it.)
Fails. While you have a sequence it is used improperly. You can not
just code a standalone name. It must be used in either a SQL
statement or an assignment statement.
Passes.
Fails. Your function passes the IN parameters correctly. However
there are no OUT parameters.
Your function returns Number not the required True/False which
requires either Boolean or varchar2 for literal.
Code section for requirements 3,4.
create or replace function dsb_add_new_users (i_name in varchar2,
i_address in varchar2,
i_birthdate in date,
i_country in varchar2) <<< where are the 2 required out parameters
return number is <<< Should be True or False so either Boolean or varchar2
o_error_message exception; <<< Should be an Out parameter not an exception
Yes you have the Exception Section, which is required for handling
exceptions. However you do not handle exceptions so much as suppress
them, indicating to Oracle, and to the calling routine, that they didn't actually happen.
exception
when o_error_message then <<< as coded this is a developer defined exception, yet is not raised so it cannot happen.
return null; <<< even if it were raised you have thrown it away, the calling routine will never it happened
when others then <<< One of the worst pieces of code that can be written. I tend to remember seeing a discussion in
return null; <<< Oracle that would make this a compile time error. To bad the supporters lost the argument.
Passed, mostly. Correct for posted code, but insufficient per requirements.
So all-in-all not highly successful when viewed against the requirements.
A couple other items not specific the requirements, but disaster to successful implementation.
While technically you might get away with the following it is very bad practice. Current_User is an Oracle reserved word.
current_user number; <<< NO, NO bad verifiable name.
The following completely decimates your function. It creates a never ending loop. Well not quite -- it runs 1 time, but never afterward.
I believe you intended for the current_user variable to contain the ID assigned to the user. But it actually contains the number of rows in the table. Further what would the number of rows (count) have to do with Inserting a row? SO what does the code actually Do?
dsb_nb_seq_user_id.nextval; <<< systax error.
select count(user_id) into current_user <<< count the number of rows in the table
from dsb_nb_users;
while current_user != 0 <<< if there are any rows in the table
loop
dsb_nb_seq_user_id.nextval; <<< syntax error
select count(user_id) into current_user <<< try counting then again until there are none.
from dsb_nb_users;
end loop;
So correcting for the above what do we wind up with. I hesitate here in fear you will just copy the below and submit it. Please don't do that but study and understand what it's doing, then rewrite you routine. But after tearing you initial effort up, I guess I owe a corrected solution.
create or replace function dsb_add_new_users
( i_name in varchar2
, i_address in varchar2
, i_birthdate in date
, i_country in varchar2
, o_new_user_id out number
, o_error_message out varchar2
return boolean
is
country_id_l number;
begin
-- initialize out variables
o_new_user_id := null;
o_error_message := null;
-- get country id from input parameter
select country_id
into country_id_l
from country
where name = i_country;
-- create user and get the assigned user_id
insert into dsb_nb_users (user_id, name, address, birthdate, country_id_fk)
values dsb_nb_seq_user_id.nextval
, i_name
, i_address
, i_birthdate
, country_id_l
)
returning user_id
into o_new_user_id;
return true;
exception
when no_date_found then
o_output_message = 'ERROR: Specified country name ''' || country_id_l || ''' Not Found.';
return false;
when others then
o_output_message := 'ERROR: ' || sqlerrm;
return false
end dsb_add_new_users ;
--------------- Test Driver ----------------
declare
i_new_user number;
new_user_created boolean;
error_message varchar2(255);
begin
new_user_created := dsb_add_new_users( i_name => 'Arnaldo Amaral',
, i_address => 'Rua da Agra'
, i_birthdate => to_date('03/05/1959','mm/dd/yyyy') -- or is it 'dd/mm/yyyy'
, i_country => 'PT'
, o_new_user_id => i_new_user
, o_error_message => error_message);
if not new_user_created
then
dbms_output.put_line (error_messag);
end if ;
end;
Disclaimer: As you did not post table DDL nor test data the routine has not been tested nor even compiled. Any syntax error(s) is for you to resolve. Further, I follow Tejash in the assumption you actually have a countries table and there is a FK to it. That may be an erroneous assumption.
Why don't you directly use the sequence value to insert new user as follows:
CREATE OR REPLACE FUNCTION DSB_ADD_NEW_USERS (
I_NAME IN VARCHAR2,
I_ADDRESS IN VARCHAR2,
I_BIRTHDATE IN DATE,
I_COUNTRY IN VARCHAR2
) RETURN NUMBER IS
CURRENT_USER_ID NUMBER;
BEGIN
CURRENT_USER_ID := DSB_NB_SEQ_USER_ID.NEXTVAL; -- returns new and unique number
INSERT INTO DSB_NB_USERS (
USER_ID,
NAME,
ADDRESS,
BIRTHDATE,
COUNTRY_ID_FK
) VALUES (
CURRENT_USER_ID,
I_NAME,
I_ADDRESS,
I_BIRTHDATE,
(
SELECT
COUNTRY_ID
FROM
COUNTRIES
WHERE
NAME = I_COUNTRY
) -- it is FK so need to use SELECT sub-query to find COUNTRY_ID
);
RETURN CURRENT_USER_ID; -- returning the new user ID
EXCEPTION
WHEN OTHERS THEN
RETURN -1; -- returning -1 in case there is any error
END;
-1 will be returned when there is an error while adding the new user.
Came up with this much simpler answer after asking to a few people for help.
create or replace FUNCTION DSB_ADD_NEW_USERS (I_NAME IN VARCHAR2,
I_ADDRESS IN VARCHAR2,
I_BIRTHDATE IN DATE,
I_COUNTRY IN VARCHAR2)
RETURN NUMBER IS
CURRENT_USER NUMBER;
BEGIN
CURRENT_USER := DSB_NB_SEQ_USER_ID.NEXTVAL;
DBMS_OUTPUT.PUT_LINE('Hello World');
INSERT INTO DSB_NB_USERS (USER_ID, NAME, ADDRESS, BIRTHDATE, COUNTRY_ID_FK) VALUES (CURRENT_USER, I_NAME, I_ADDRESS, TO_DATE(I_BIRTHDATE, 'DD/MM/YYYY'), I_COUNTRY);
RETURN CURRENT_USER;
EXCEPTION
WHEN OTHERS THEN
RETURN -1;
END;
SET SERVEROUT ON
DECLARE
I_NEW_USER NUMBER;
BEGIN
I_NEW_USER := DSB_ADD_NEW_USERS(I_NAME => 'Arnaldo Amaral',
I_ADDRESS => 'Rua da Agra',
I_BIRTHDATE => '03/MAY/1959',
I_COUNTRY => 'PT');
commit;
END;
/
Thank you all!!!
I am using Oracle 11g in Windows environment. I am not a thorough PL/SQL Developer. My situation is like this.
I am using a package, need to validate the logging in user. Not checking table column directly to do this.
create or replace package Configuration_pkg as
TYPE user_rec IS RECORD
(email VARCHAR2(120),
password VARCHAR2(120));
TYPE user_tab IS TABLE OF user_rec INDEX BY BINARY_INTEGER;
function Validate_logged_user (p_user_tab IN user_tab) RETURN VARCHAR2;
end Configuration_pkg;
create or replace package body Configuration_pkg as
function Validate_logged_user (p_user_tab IN user_tab) RETURN VARCHAR2 IS
Ismatching number;
begin
select count(1)
into Ismatching
from CG_M_USERS
where username = user_tab.email
and password = user_tab.password;
if Ismatching = 0 then
return 'Invalid username / password';
elsif Ismatching = 1 then
return 'Login successful';
end if;
end Validate_logged_user;
end Configuration_pkg;
I am getting the following error
Error(10,20): PL/SQL: ORA-00904: "USER_TAB"."PASSWORD": invalid identifier
Error(10,29): PLS-00302: component 'PASSWORD' must be declared
I want to validate the user with the value passed with the record type, not directly checking username and password from table. Everyone's help is highly appreciated.
There are few mistakes in your code.
1) You are using a Oracle reserve keyword 'PASSWORD'.
2) You are passing a collection to the function. So you need to run a loop to get the values of the collection.
See the revised complied code.
CREATE OR REPLACE PACKAGE Configuration_pkg
AS
TYPE user_rec IS RECORD
(
email VARCHAR2 (120),
passwrd VARCHAR2 (120)
);
TYPE user_tab IS TABLE OF user_rec INDEX BY BINARY_INTEGER;
FUNCTION Validate_logged_user (p_user_tab IN user_tab)
RETURN VARCHAR2;
END Configuration_pkg;
----------------------------------------
/
CREATE OR REPLACE PACKAGE BODY Configuration_pkg
IS
FUNCTION Validate_logged_user (p_user_tab IN user_tab)
RETURN VARCHAR2
IS
Ismatching NUMBER;
msg1 varchar2(20):= 'Invalid username/passwrd';
msg2 varchar2(20):= 'Login successful';
BEGIN
for r in 1..p_user_tab.count
loop
SELECT COUNT (1)
INTO Ismatching
FROM CG_M_USERS
WHERE username = p_user_tab(r).email
AND passwrd = p_user_tab(r).passwrd;
IF Ismatching = 0
THEN
RETURN msg1;
ELSIF Ismatching = 1
THEN
RETURN msg2;
END IF;
end loop;
END Validate_logged_user;
END Configuration_pkg;
when the following code is running exception statements are not rising automatically by the database server it's Happen when I enter wrong userid value or when the value is null , I don't want to use RAISE LOGIN_DENIED; explicitly in my code , so what do you think? am I missing something?
CREATE OR REPLACE PROCEDURE user_auth(
userid IN st_az.st_name%type ,
pass OUT st_az.st_pass%type ,
message OUT varchar2 ,
err_msg OUT varchar2 ) IS
BEGIN
message:= 'login is done successfully';
err_msg:= 'Login Denied .. Please Try Again!';
SELECT st_pass INTO pass FROM st_az WHERE st_name = userid ;
dbms_output.put_line(message);
EXCEPTION
WHEN LOGIN_DENIED THEN
dbms_output.put_line(err_msg);
END user_auth;
If you want to find info in st_az table and raise an error when there is no such row, you need NO_DATA_FOUND exception
CREATE OR REPLACE PROCEDURE user_auth(
userid IN st_az.st_name%type ,
pass OUT st_az.st_pass%type ,
message OUT varchar2 ,
err_msg OUT varchar2 ) IS
BEGIN
message:= 'login is done successfully';
err_msg:= 'Login Denied .. Please Try Again!';
SELECT st_pass INTO pass FROM st_az WHERE st_name = userid ;
dbms_output.put_line(message);
EXCEPTION
WHEN NO_DATA_FOUND THEN
dbms_output.put_line(err_msg);
END user_auth;
Your original code of the question needs some modification. The actual parameter that corresponds to an OUT formal parameter must be a variable; it cannot be a constant or an expression.
Now, if you don't want to use LOGIN_DENIED then you can either try using NO_DATA_FOUND or simple generalized OTHERS in exception block.
The code may be like this--
CREATE OR REPLACE
PROCEDURE user_auth(
userid IN st_az.st_name%type ,
pass OUT st_az.st_pass%type ,
MESSAGE OUT VARCHAR2 ,
err_msg OUT VARCHAR2 )
AS
message1 VARCHAR2(50);
err_message1 VARCHAR2(50);
BEGIN
message1 := 'login is done successfully';
err_message1:= 'Login Denied .. Please Try Again!';
SELECT st_pass INTO pass FROM st_az WHERE st_name = userid ;
MESSAGE:= message1;
dbms_output.put_line(MESSAGE);
EXCEPTION
WHEN OTHERS THEN
err_msg:= err_message1;
dbms_output.put_line(err_msg);
END user_auth;
Now testing above code:--
create table st_az(st_name varchar2(10),st_pass varchar2(10));
insert into st_az values ('aa','aa');
insert into st_az values ('bb','bb');
commit;
Creating anonymous block and call procedure:-
DECLARE
MESSAGE VARCHAR2(50);
err_msg VARCHAR2(50);
pass VARCHAR2(10);
BEGIN
user_auth('cc',pass,MESSAGE,err_msg);
END;
I need to get the output in uu in accordance with value passed through the prompt
create or replace procedure chklg( uu out logn.username%TYPE
, pass in logn.password%TYPE)
is
begin
select username into uu from logn where password=pass;
end;
I tried executing the above procedure this way:
begin
chklg(:pass);
end
By definition a procedure doesn't return anything. You're looking for a function.
create or replace function chklg ( p_pass in logn.password%TYPE
) return varchar2 is -- assuming that logn.username%TYP is a varchar2
l_uu logn.username%type;
begin
select username into l_uu from logn where password = p_pass;
return l_uu;
-- If there-s no username that matches the password return null.
exception when no_data_found then
return null;
end;
I'm slightly worried by this as it appears as though you're storing a password as plain text. This is not best practice.
You should be storing a salted and peppered hash of your password next to the username, then apply the same salting, peppering and hashing to the password and select the hash from the database.
You can execute the function either of the following two ways:
select chklg(:pass) from dual
or
declare
l_pass logn.password%type;
begin
l_pass := chklg(:pass);
end;
/
To be complete Frank Schmitt has raised a very valid point in the comments. In addition to you storing the passwords in a very dangerous manner what happens if two users have the same password?
You will get a TOO_MANY_ROWS exception raised in your SELECT INTO .... This means that too many rows are returned to the variable. It would be better if you passed the username in as well.
This could make your function look something like the following
create or replace function chklg (
p_password_hash in logn.password%type
, p_username in logn.username%type
) return number
/* Authenticate a user, return 1/0 depending on whether they have
entered the correct password.
*/
l_yes number := 0;
begin
-- Assumes that username is unique.
select 1 into l_yes
from logn
where password_hash = p_password_hash
and username = p_username;
return l_yes;
-- If there-s no username that matches the password return 0.
exception when no_data_found then
return 0;
end;
If you're looking to only use a procedure (there's no real reason to do this at all as it unnecessarily restricts you; you're not doing any DML) then you can get the output parameter but you have to give the procedure a parameter that it can populate.
In your case it would look something like this.
declare
l_uu logn.username%type;
begin
chklg(l_uu, :pass);
dbms_output.put_line(l_uu);
end;