You are not logged in.
Pages: 1
I am restarting this discussion in a new thread to cover the implementation of OAuth2. The original discussion was here.
I know there is at least some interest in this. My submission in the previous thread has gone quiet. Not sure if it's because my work is terrible, incomplete or both? I am hoping it is just because it is not immediately clear as to how to get OAuth2 up and running easily, and it is this that I will address in this discussion.
tldr; For the really impatient:
Get the core implemetation: OAuth2Authentication.pas
Include OAuth2Authentication.pas in you Delphi project
Derive a class from one of the 4 grant types:
TOAuth2AuthorizationGrant
TOAuth2ImplicitGrant
TOAuth2ResourceOwnerPasswordGrant - (example based on this one)
TOAuth2ClientCredentialsGrant
Override the minimum required methods:
GetAuthToken() - you validate credentials and return a token
GetSession() - you validate the token and return a TAuthSession (or descendant)
AuthSuccess() - you store the auth_token, refresh_token and expires for future use (authenticating subsequent requests)
//TODO: Refresh token handling goes here
In your TSQLRestServer (or descendant) constructor register your authentication class:
constructor TMySQLRestServer.Create(aModel: TSQLModel);
begin
inherited;
AuthenticationRegister([TMyAuthentication]);
end;
Incorporate the 3 changes below into mORMot.pas
The example
Let me first briefly outline my requirements. We use mORMot only as a server to provide json resources from an existing, large, legacy DB2 database. The client is an Android application but there is the expectation that the mORMot server with provide resources for other integration's. With that said I have done some pretty extensive modifications to the server (via descendants) to provide a flexible architecture for our developers to create new server (endpoints) and extend existing endpoints with additional resources.
Our system is also internal (for now) and is expected to operate within our customers corporate network (or VPN). This affects security considerations.
Authentication
Although the OAuth2 implementation is not complete, it follows the spec (https://tools.ietf.org/html/rfc6749) to the letter.
AFAIK, the OAuth2 authentication process is similar to the mORMot system.
The spec outlines 4 grant types. The first, Authorization Code Grant, is probably the most used for web site collaboration and the primary use case envisioned by the creators of this. However, in my situation I want to connect with an existing user/pass database and provide authentication tokens. This is a common pattern and is supported well on Android. OAuth2 provides 3 alternative grant types to support additional cases, after reviewing the grant types I decided on "Resource Owner Password Grant", IMHO this matches both our needs and matches the mORMot authentication system well.
The process occurs like so:
In order to access a resource the client must have an Authorization bearer token in the HTTP header e.g.:
AUTHORIZATION: Bearer FD94A9F2D4644F619CD3705B98371EF2
if this is not present the server will return "401 Unauthorized" (currently mORMot would produce 403 Forbidden). The header will contain a
WWW-Authenticate: Bearer realm="mORMot Server"
The body will contain a json document with 1 to 3 data elements:
{
error: "Unauthorized" //REQUIRED
error_description: "Something more descriptive" //OPTIONAL
error_uri: "uri for more info about this error" //OPTIONAL
}
In order to get a token the client must request a token at the /auth endpoint (same as mORMot?). Include a standard basic authentication header and also include the user/pass in the body (Obviously a secure connection is recommended for this). Personally I don't understand why they want the credentials twice. This implementation looks at the "Authorization: Basic ..." header only.
A successful call to /auth will result in something like:
{
"access_token": "0C49867F502F455CA300916F158E6CDB",
"token_type": "bearer",
"expires_in": 86400,
"refresh_token": "00F075FB3AE541E2984345CF88470A00"
}
The client should store that access_token and use it as the bearer token until another 401 is received at which point the refresh token logic should kick in.
I would like to make this a full and complete implementation. But... I am not going to bother if there is no interest. I expect to start the refresh token logic very soon. as for client support and other grant types, that will depend on interest.
Best regards,
Jeff
PS: Just realized there was something missing from my original post:
Add a new method to TSQLRestServerAuthentication (to allow each authentication class to handle failures individually)
TSQLRestServerAuthentication = class
...
public
...
function AuthenticationFailed(Ctxt: TSQLRestServerURIContext): boolean; virtual;
end;
is implemeted like so (to ensure backwards compatibility):
function TSQLRestServerAuthentication.AuthenticationFailed(Ctxt: TSQLRestServerURIContext): boolean;
begin
Ctxt.Call.OutStatus := HTML_FORBIDDEN;
Result := True;
end;
then in TSQLRestServerURIContext we modify AuthenticationFailed() to handle multiple authentication instances:
procedure TSQLRestServerURIContext.AuthenticationFailed;
var
aSession: TAuthSession;
i: integer;
begin
//JGB
if Server.HandleAuthentication then
begin
Session := CONST_AUTHENTICATION_SESSION_NOT_STARTED;
Server.fSessions.Lock;
try
aSession := nil;
if Server.fSessionAuthentication<>nil then
for i := 0 to Server.fSessionAuthentications.Count-1 do
if Server.fSessionAuthentication[i].AuthenticationFailed(Self) then
Break;
finally
Server.fSessions.UnLock;
end;
end;
// 401 Unauthorized response MUST include a WWW-Authenticate header,
// which is not what we used, so here we won't send 401 error code but 403
//JGB - We handle the out status in the TSQLRestServerAuthentication classes
//JGB - Call.OutStatus := HTML_FORBIDDEN;
end;
The TOAuth2AbstractAuthentication class uses this method to return authentication failures consistent with the OAuth2 spec.
The base implementation is here:
https://gist.github.com/jbendixsen/392e … cation-pas
My pseudo code example of the password grant scheme:
https://gist.github.com/jbendixsen/5b42 … cation-pas
So it occurred to me that I have still yet to do the token expiration logic and refresh token handling. This is definitely something I want to add so I can work on that soon. Still the core functionality is there.
My apologies for not having something more complete.
This is working, though not 100% complete. How do I go about contributing this?
The situation is this: there are 4 schemes in the OAuth spec. I only needed one (for now). I have a file that implements the 4 schemes in a way that lets users extend the scheme they want to use and implement security that is appropriate for the situation. The default behavior should validate against the standard mORMot users in a way that's consistent the existing security. I have roughed this in but not tested it (time). Also to be fully complete the mORMot clients should support this, also something I have not done, nor do I have much desire to. I have spent virtually no time working with the mORMot client.
So my delay in contributing this back is these few incomplete items. However, the actually OAuth2 part is all working well. I am sure there will be some rework or that I might have missed something but I think it is certainly ready for those who are interested.
I have a question and sort of thinking out loud regarding the OAuth2 implementation....
First it seems to me that the existing session management does not persist? As I try to plug in the OAuth2 workflow I am looking at RetrieveSession(), SessionCreate() and SessionAccess() to name a few. A session is only created when an /auth call is issued. So, for instance , if the server was restarted all sessions would be invalidated?
In the OAuth2 world session persistence (or Access Token validity) and server persistence are not connected. This is fine for my derived implementation (I store the Access Token in a database table) but does not work well for having a "default" OAuth2 scheme.
For the actual OAuth2 implementation, there is a new abstract class:
TOAuth2AbstractAuthentication = class(TSQLRestServerAuthentication)
...
end;
OAuth2 has 4 grant types:
Authorization Code Grant
Implicit Grant
Resource Owner Password Credentials Grant
Client Credentials Grant
I only expect to support Resource Owner Password Credentials Grant in my project for now. Though it doesn't seem to make sense to try to contribute back to mORMot nothing but a complete implementation.
There are four derived classes to match each grant type:
// Authorization Code Grant
// - http://tools.ietf.org/html/rfc6749#section-4.1
TOAuth2AuthorizationGrant = class(TOAuth2AbstractAuthentication)
...
end;
// Implicit Grant
// - http://tools.ietf.org/html/rfc6749#section-4.2
TOAuth2ImplicitGrant = class(TOAuth2AbstractAuthentication)
...
end;
// Resource Owner Password Credentials Grant
// - http://tools.ietf.org/html/rfc6749#section-4.3
TOAuth2ResourceOwnerPasswordGrant = class(TOAuth2AbstractAuthentication)
...
end;
// Client Credentials Grant
// - http://tools.ietf.org/html/rfc6749#section-4.4
TOAuth2ClientCredentialsGrant = class(TOAuth2AbstractAuthentication)
...
end;
Each of these implement the OAuth2 request/response as per the spec. In my case, I need... well I envision at least 3 different type of user/password validation but for now I'll implement validation against credentials stored in a pre-existing database table.
So for custom authentication and access token generation I use a descendant of the desired grant type like so:
TMyAuthentication = class(TOAuth2ResourceOwnerPasswordGrant)
...
end;
then in my custom server class (TMyServer = class(TSQLRestServer)) in the constructor:
constructor TMyRestServer.Create(aModel: TSQLModel);
begin
//Call inherited without registering default authentication
inherited Create(AModel, False);
//Register custom authentication, system is designed to support multiple
AuthenticationRegister([TMyAuthentication]);
...
end;
Now in my derived authentication class along with a derived server and custom database access I can validate credentials, generate access tokens and validate access tokens etc.. in a very custom way from my legacy database system.
The AuthenticatioFailed() override you added at my request got a little more complicated.
I added:
TSQLRestServerAuthentication = class
...
function AuthenticationFailed(Ctxt: TSQLRestServerURIContext): boolean; virtual;
end;
and then enhanced the new AuthenticationFailed() to push the error handling to each Authentication class
procedure TSQLRestServerURIContext.AuthenticationFailed;
var
aSession: TAuthSession;
i: integer;
begin
//JGB
if Server.HandleAuthentication then
begin
Session := CONST_AUTHENTICATION_SESSION_NOT_STARTED;
Server.fSessions.Lock;
try
aSession := nil;
if Server.fSessionAuthentication<>nil then
for i := 0 to Server.fSessionAuthentications.Count-1 do
if Server.fSessionAuthentication[i].AuthenticationFailed(Self) then
Break;
finally
Server.fSessions.UnLock;
end;
end;
// 401 Unauthorized response MUST include a WWW-Authenticate header,
// which is not what we used, so here we won't send 401 error code but 403
//JGB - We handle the out status in the TSQLRestServerAuthentication classes
//JGB - Call.OutStatus := HTML_FORBIDDEN;
end;
This is not finished. It needs to handle multiple authentication schemes better.
Work is ongoing...
As always I look forward to any feedback.
Regards,
Jeff
I discovered this strange problem when I used attributes to on the TSQLRecord properties to override the JSON field names.
Consider:
...
published
[TFieldAttribute('firstName')]
property FIRST_NAME;
...
These attribute values where not getting picked up. After a lot of digging I came across this line in SynCommons.pas (line 13649):
AllCount: Integer; // !!!! may need {$RTTI EXPLICIT FIELDS([vcPublic])}
if I change to:
AllCount: Integer; // !!!! may need {$.RTTI EXPLICIT FIELDS([vcPublic])}
the problem goes away.
I cannot duplicate this every time, but when I update the mORMot source and build the symptom of this problem occurs and adding the period resolves the problem.
Which sort of makes sense, because according to the documentation they way that the RTTI directive is used there it would hide published... everything. What doesn't make sense is that it is commented out so I wonder if this is a bug in Delphi 2010.
Yeah, wow! Now that's service.
While it could be that this is all that is needed. I will need to actually implement this first. I'll report back soon....
Thank you for the quick response
Hi,
First post! Using mORMot I've developed a REST/JSON server for and existing and LARGE product. Our intention is to not only use this mORMot server for the server side of an Android app but also possibly as a general API server.
Over the last few months I've extended mORMot to suite our needs. Because we have a large legacy system many of the built in niceties don't apply and I've had to diverge from a typical mORMot implementation. Fortunately, the framework is written well enough to allow extending the code base to suite our needs without having to make changes to any of the mORMot code.
The use case we have is the "not yet implemented" ORM case where we have externally mapped tables not managed by the framework (see SAD - 1.4.1.9.2. Several ORMs at once, 3rd bullet). I would happily describe in more detail my implementation but that's not why I am posting this...
I'd like to implement OAuth2 authentication, the "Resource Owner Password Credentials Grant" grant type for now(http://tools.ietf.org/html/rfc6749#page-37). The problem I have is that I there is one spot in the mORMot code that could be improved to allow for better authorization scheme extensibility.
I'll show the problem and propose a solution but I am also open to alternatives.
in mormot.pas the method TSQLRestServer.URI has something like this:
...
// 2. handle security
if (not Ctxt.Authenticate) or
((Ctxt.Service<>nil) and
not (reService in Call.RestAccessRights^.AllowRemoteExecute)) then
// 401 Unauthorized response MUST include a WWW-Authenticate header,
// which is not what we used, so we won't send 401 error code but 403
Call.OutStatus := HTML_FORBIDDEN else
...
The problem is, as the comment states, I need to return 401 and add the WWW-Authenticate header. I could/can get around this by adding specialized logic to the TSQLRestServerURIContext descendant I already use for other aspects of my implementation. However, it's not very pretty. Though I haven't actually coded the solution what I'd like to see is:
1) Add a virtual method (e.g. HandleUnauthorized() ) to TSQLRestServerURIContext that sets up Call.OutStatus et al to an appropriate response.
2) The default behavior would be: Call.OutStatus := HTML_FORBIDDEN to be non-breaking
3) Change the above Call.OutStatus := HTML_FORBIDDEN to Ctxt.HandleUnauthorized()
The remaining OAuth2 implementation will go into new TSQLRestServerAuthentication descendants.
I look forward to any feedback.
Regards,
Jeff
Pages: 1