AAVSO / VStar

VStar is a visualisation and analysis tool for variable star data brought to you by AAVSO
https://www.aavso.org/vstar
GNU Affero General Public License v3.0
9 stars 3 forks source link

Move to Auth0 #375

Closed dbenn closed 9 months ago

dbenn commented 11 months ago

AAVSO is moving from OAuth to Auth0.

What endpoints are affected?

Just auth api? Also VSX, VSP? I suspect just the former.

aru-py commented 11 months ago

The transition will affect all our auth related urls including login, sign-up, and password reset. Urls specific to VSX and VSP should not be affected.

dbenn commented 11 months ago

Thanks @aru-py, so just where we currently use https://www.aavso.org/apps/api-auth/ ?

It's used here: https://github.com/AAVSO/VStar/blob/master/src/org/aavso/tools/vstar/auth/AAVSOPostAuthenticationSource.java#L45

Can you provide an example of code (doesn't have to be Java; Python or any other language is fine) of how use of Auth0 will be used for login?

VStar only requires authentication via username and password for a couple of reasons:

dbenn commented 11 months ago

Useful references

aru-py commented 11 months ago

@dbenn I've integrated Auth0 with our Django repositories. Would it be useful to you to take a look at those? If you don't have access, @bpablo may be able to give grant viewing permissions.

dbenn commented 11 months ago

That would be helpful. I really just need to be pointed to an example of Auth0 client integration. If in Django, sure.

I will also look at https://auth0.com/docs/quickstart/webapp/java/01-login (for example).

aru-py commented 11 months ago

Okay, I'll check with @bpablo to see if it's possible to give you read-only permission for one of our Django repositories. The link you provided, along with the flow overview I sent earlier will be very useful.

To provide a quick overview, your application will redirect to Auth0 (based on a client_id and domain which I’ll provide), and upon login, it will call back to your application with a code in the url that can be exchanged for authentication tokens. You can try it for yourself when logging in at apps.aavso.org/campaigns.

You should also note that OAuth 2.0 is a complete rewrite of OAuth 1.0 (https://www.oauth.com/oauth2-servers/differences-between-oauth-1-2/).

dbenn commented 11 months ago

Thanks @aru-py. @bpablo has given me access to https://github.com/AAVSO/django_site

Can you pointy me to some specific code there?

aru-py commented 11 months ago

Yes, this should be your entry point: https://github.com/AAVSO/django_site/blob/auth0-integration/urls.py.

These are all the authentication related urls (you may ignore the ones related to silent authentication for right now). Each view should have all the authentication functionality for you to look at. Let me know if you need anything!

  path("accounts/login/", CustomLoginView.as_view(), name="account_login"),
  path("accounts/logout/", CustomLogoutView.as_view(), name="account_logout"),
  path("accounts/signup/", CustomSignUpView.as_view(), name="account_signup"),
  path('silent-auth-callback/', SilentAuthCallback.as_view(), name='silent_auth_callback'),
  path('silent-auth-trigger/', SilentAuthTrigger.as_view(), name='silent_auth_trigger'),
  path("accounts/", include("allauth.urls")),

(also, make sure you're on the auth0-integration branch)

dbenn commented 11 months ago

Looking at the diagram on https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow @aru-py, I think the steps I need are 4 to 8.

In VStar (which is a desktop application, not web-based), access to member-only plugins (like APASS) or reporting observations as discrepant start with a username/password dialog:

Screenshot 2023-11-02 at 09 52 17

So other than username and password, a domain and client_id will be necessary from what you've said above. Also a client secret of some kind I think.

aru-py commented 11 months ago

Since your application is desktop-based, you may have to use a different flow, as you do not want to code the secret into the application.

This flow can only be used for confidential applications (such as Regular Web Applications) because the application's authentication methods are included in the exchange and must be kept secure.

I will email you the domain and client_id, however.

dbenn commented 11 months ago

@aru-py, it may help us reason about this if we come back up to the 50,000 foot level, the bird's-eye view, and think about what we're trying to achieve here.

In VStar, the purpose of authenticating is to answer the questions:

If both are true, VStar allows access to member-only plugins or discrepant observation reporting.

In fact, the only thing VStar really "cares about" for these purposes is whether the user is an AAVSO member. Authentication is merely a necessary pre-cursor to being able to answer that question.

https://github.com/AAVSO/VStar/blob/master/src/org/aavso/tools/vstar/auth/AAVSOPostAuthenticationSource.java#L70 implements an IAuthenticationSource method authenticate which internally answers these two questions.

So, here's a question for you. Given that we really don't want to or indeed, can't, expose the secret in the application, would it make sense for you to expose a REST API function that takes a (probably obfuscated, e.g. base64 encoded or perhaps something better so as not to pass plaintext over the wire) username and password +/- client_id and domain and allows all the rest of the flow to be hidden behind a web app on the server?

The only thing I would need in return is:

There is one other thing to realise about what we currently do actually.

The authenticate method calls retrieveUserInfo here:

https://github.com/AAVSO/VStar/blob/master/src/org/aavso/tools/vstar/auth/VSXWebServiceMemberInfo.java#L48

passing a user ID (a numeric value presumably from a membership database) obtained from the authentication POST response here:

https://github.com/AAVSO/VStar/blob/master/src/org/aavso/tools/vstar/auth/AAVSOPostAuthenticationSource.java#L142

It's this retrieveUserInfo that tells us whether the user is an AAVSO member. It also gets us their observer code. Right now, this endpoint provides this information, e.g. for some user ID 1153:

https://www.aavso.org/vsx/index.php?view=api.member&id=1153

which may not pass muster now either. Are you aware of other applications using this internally?

There really should be no need for this user ID or this VSX endpoint to be exposed.

The main issues seem to be:

If I was able to take a simple approach in VStar of passing username and password (obfuscated rather than in plaintext) via a REST API GET call and obtain is-authenticated, is-member, and observer code, that's all I would need and all the secret stuff would be kept server-side.

Thoughts?

Note: it's this sort of hidden complexity that requires time to work through, implement and test of course.

dbenn commented 11 months ago

Adding you to this @mpyat2 so you can see what's going on. Mostly discussion and links up to now.

aru-py commented 11 months ago

@dbenn I wanted to let you know that I'm currently traveling (for the annual meeting) and will try to get back to you as soon as possible

aru-py commented 11 months ago
image

@dbenn I also am having some trouble getting the application to run on my MacBook (using version Venture 13.6)

aru-py commented 11 months ago

Regarding VSX, I’m not aware of any other use of that endpoint, but it might be worth checking with @bpablo.

I think you have a good idea! which We might even be able to leverage this with some of our other legacy applications. However, one of the challenges with your proposal is that authentication is required to be done through the Auth0 web interface, for a variety of reasons, including security and supporting social account logins (e.g. Google), so simply passing the username/password would not work.

Instead, I suggest that we redirect users from the Java application to a url (such as apps.aavso.org/auth/external?app=vstar), which after the user logs in, will return a temporary code. Users will provide this code to the app, which can then be exchanged for an auth token, using a POST request.

This will allow us to use the Auth0 login without the effort required to manually implement the authorization flow.

@dbenn Curious to hear your thoughts.

dbenn commented 11 months ago

That sounds good @aru-py!

When you say, "redirect users from the Java application to a url", do you mean, have the Java code open a connection to that URL, pass user name and password etc or have the Java code open a web browser to that URL, login, get a code that the user copies and pastes into a dialog or...? I suspect you mean the first of these.

Happy to talk in more detail as you have time.

dbenn commented 11 months ago
image

@dbenn I also am having some trouble getting the application to run on my MacBook (using version Venture 13.6)

To open VStar, hold down the control key and click the mouse to yield this dialog:

Screenshot 2023-11-05 at 18 41 21

Then you can click Open.

aru-py commented 11 months ago

@dbenn The application works now.

I suggested the second one: having the Java code open the browser, login, and provide the code to the application which can then be exchanged for an authentication token. The advantage for this method would be that if the browser is already authenticated, the user will not need to login again and the code will be made available immediately.

Would there be any advantage in having Java create a connection? For example, would it be able to extract the code automatically from the browser so the user does not have to enter it manually?

Yes, happy to talk when you have time. I should be available today for a call.

dbenn commented 11 months ago

Hi @aru-py

Glad the app opens now.

With the first option, would the code appear in the web browser to be copied and pasted by the user into a VStar dialog? Could explore the option of having Java make the connection although I suspect it may be more complicated and time is short before the Nov 20 Auth0 go-live date.

So, a focus at least in the short term on the simplest approach to getting the code back to VStar would make sense. Thereafter, alternatives could be considered.

Happy to have a call but we'll have to think about a suitable time given the TZ delta.

aru-py commented 11 months ago

Yes, with the first option the code would be copied and pasted by the user into the VStar dialog. We could start with the first option and then later add the Java connection to make it more seamless. I can start working on the backend if this approach looks reasonable to you.

dbenn commented 11 months ago

Yes please @aru-py!

aru-py commented 11 months ago

@dbenn I'm traveling until Nov. 15th, but I should have a chance to work on it over the next week.

aru-py commented 10 months ago

@dbenn I’ve got an initial version implemented! The flow is as follows:

  1. Send the user to http://apps.aavso.org/auth/external passing in a query parameter identifier which is a random uuid generated by your application. Once user logs in, they will be presented with a six digit code.

  2. The user enters the code in your application, which along with the identifier, is sent to a POST to the same endpoint, returning a session token that can be used for authenticating requests.

See the example below, and let me know if you have any questions. I believe there is additional data you will want to have returned by the POST request.


# Get Code
GET http://apps.aavso.org/auth/external?identifier=1234567890

# Exchange Code for Token
POST http://dev.apps.aavso.org/auth/external
Content-Type: application/json
Accept: application/json

{
 "code": 155950,
 "identifier": "1234567890"
}
dbenn commented 10 months ago

Thanks @aru-py. I will start implementing the client of this in VStar and get back to you ASAP.

I'm still trying to work towards a Nov 20 deadline although I suspect a VStar release will lag awhile after that.

Thanks for your efforts so far!

dbenn commented 10 months ago

@aru-py I have some questions.

Currently when logging into AAVSO you enter username and password:

image

whereas this page (http://apps.aavso.org/auth/external) is asking for email and password. Is there a sign up process required? In any case, it currently fails for me (actual email address obscured):

image

I notice that the auth URL uses http not https. Will that change?

In the GET example you give:

# Get Code
GET http://apps.aavso.org/auth/external?identifier=1234567890

can this actually ever be an operation to get the code given the identifier since interactive login is required?

The POST operation makes sense. As you say, as well as the token, I'll also need to get is-member status and observer code. Will that happen with a 3rd exchange (GET or POST?) that uses the token?

Thanks.

aru-py commented 10 months ago

@dbenn

(1) You will have to create an account with Auth0 (sign up option should be the login page). We will be migrating existing user accounts during our rollout.

(2) Yes, http should automatically redirect to https.

(2) You should provide the GET link in your application, which will open it up in the user's browsers. The users will type the code into your application. Also, the identifier is used for additional security purposes – it's possible to implement this flow without it, but it adds another layer of security so only your application instance can use the generated code.

(4) Is there any additional API requests you make from your application? If not, it would be easier to provide some user information (such as member status, obscode) as part of the POST response.

dbenn commented 10 months ago

That makes sense @aru-py.

Additional info (is-member status and observer code) via the POST response would be great, thanks.

By the way, I see this on Mac OS X Safari 16.5:

image

I cannot see a problem in settings. Cookies are not being blocked:

image

Chrome and Firefox work fine though. The problem is that a user will be redirected to their default browser which in my case is Safari. Should I also show the link that I'm programmatically redirecting them to in case they need to paste it into a different browser?

aru-py commented 10 months ago

@dbenn I'll work on adding those fields to the POST response.

Do you get this error during the GET request? I'll take a look into the cause, but it should be okay if we're not able to find a solution for you to also provide the url somewhere with a message such as "in case you're experiencing issues, paste this link into Chrome or Firefox". I have disabled CSRF for those endpoints, so it is odd.

dbenn commented 10 months ago

@aru-py thanks re: POST response.

Yes, during GET, even simply by pasting the URL into Safari. Chrome, FF are OK.

aru-py commented 10 months ago

@dbenn I've updated the api to now include the obscode and is_member in the response. Let me know if it works.

I did some research, and I believe you need to disable "Prevent cross-site tracking" in Safari. See this thread: https://forum.djangoproject.com/t/safari-not-including-csrf-cookie-in-post-request/14380/2

dbenn commented 10 months ago

Thanks @aru-py. I disabled it and still see the error: image

Will let you know about obscode and is_member. Thanks!

dbenn commented 10 months ago

Actually @aru-py, it just started working!

It seems to have coincided with removing aavso.org from a list in the Privacy settings there as well. It appeared again later after going to http://apps.aavso.org/auth/external

Anyway, it seems to be working. Also, I can see the code appear after login!

More later.

Thanks!

aru-py commented 10 months ago

@dbenn Awesome, glad it's working now!

I just noticed that I had a typo in an earlier code (post should be to apps.aavso.org not dev.apps.aavso.org).

Two minor changes: for the identifier, to standardize them across apps and ensure they are secure enough, we could use UUID v4. I'm also considering adding an app url parameter to the GET request to allow app-specific responses and more security. What are your thoughts?

# Get Code
GET https://apps.aavso.org/auth/external?app=vstar&identifier=b4951d75-a0e6-4d2d-a718-0fad0a0c4bae

# Exchange Code for Token
POST https://apps.aavso.org/auth/external
Content-Type: application/json
Accept: application/json

{
 "code": 155950,
 "identifier": "b4951d75-a0e6-4d2d-a718-0fad0a0c4bae"
}
gasilvis commented 10 months ago

Cheers David @dbenn I'm getting the Auth0 to work with the java webstart app SunEntry. One step of dealing with the json return is puzzling me.

` Object ro = JSONValue.parse(responseString); JSONObject rj = (JSONObject)ro;

            se.setAUTH_email((String)rj.get("email"));            
            se.setAUTH_obscode((String)rj.get("obscode"));            
            se.setAUTH_token((String)rj.get("token"));  
            se.setAUTH_is_member((Boolean)rj.get("is_member")); 
            //Integer x= (Integer) rj.get("user_id");
            //se.setAUTH_user_id((Integer)rj.get("user_id"));             

` I cannot read out the "user_id" field which is clearly an Integer. It throws a fault. Boolean and String fields are fine.

Have you got past that?

dbenn commented 10 months ago

Hi @gasilvis, @aru-py

Apologies for the delay.

I have just reached the point of getting the basic code working. Since I didn't want to add new libraries just for this and because VStar currently assumes no higher than Java 8 is required, I wrote this simple POST JSON response processing code:

    /**
     * Given a JSON string, return a map of keys to value strings.
     * 
     * @param json A JSON string.
     * @return A mapping from key to string value.
     */
    public static Map<String, String> parseJSONString(String json) {
        Map<String, String> key2ValueMap = new HashMap<String, String>();

        json = json.trim().replace("{", "").replace("}", "");

        StringTokenizer pairLexer = new StringTokenizer(json, ",");
        while (pairLexer.hasMoreElements()) {
            String pairStr = (String) pairLexer.nextElement();
            String[] pair = pairStr.split(":\\s+");
            key2ValueMap.put(
                    trimAndRemoveQuotes(pair[0]),
                    trimAndRemoveQuotes(pair[1].trim()));
        }

        return key2ValueMap;
    }

    private static String removeQuotesAndWS(String str) {
        return str.trim().replace("\"", "");
    }

and I call it like this:

        // Populate login info from a map of JSON key-string value pairs.
        Map<String, String> results = parseJSONString(responseJSON);
        LoginInfo info = ResourceAccessor.getLoginInfo();
        info.setMember(Boolean.parseBoolean(results.get("is_member")));
        info.setObserverCode(results.get("obscode"));
        info.setUserName(results.get("email"));
        info.setToken(results.get("token"));
        info.setType(getLoginType());

I didn't initially have a need for user_id but ran into an error for this because I had no removed quotes from "user_id" or any key before storing in the map. In any case, this works now:

int m = Integer.parseInt(map.get("user_id"));

The unremoved quotes problem did not show up for:

info.setMember(Boolean.parseBoolean(results.get("is_member")));

because even though "is_member" was the key when it should have been is_member, null is treated as false by Boolean.parseBoolean(), which is not good in my view.

Feel free to use this. I have yet to commit this code but will soon. Feel free to use the code above if it helps.

dbenn commented 10 months ago

@aru-py Thanks for your latest comment. Yes, I'm happy to go with all of that including UUID v4, e.g. see https://www.uuidgenerator.net/dev-corner/java

Since I have the POST code working now I'll work on integration at the GUI level next:

I'm going on a bike riding trip for 2 or 3 days from late tomorrow so I'll finish this part later in the weekend or the first couple of days next week.

aru-py commented 10 months ago

@dbenn Perfect. I hope you enjoy your biking trip!

gasilvis commented 10 months ago

@aru-py Thanks for that json decode snippet. Saved me from having to use and external library. I have the Auth0 working from my computer, but cannot get pass "Application blocked by Java security" when the jar is put on dev-mintaka and called by the jnlp. @dbenn I've gone through the Java Console settings and they look good. And the jar file is signed

gasilvis commented 10 months ago

The jar is signed with an out-of-date cert. I assume this the reason for the block. Bert says he has a new cert available.

@aru-py I'm still puzzled why we want the user to hand carry the 6 digit number from a webpage back to the app. If the get call includes a format=json then we could go right to the put that does the sign in. eg https://apps.aavso.org/auth/external?app=sunentry&format=json&identifier=daf4204c-8126-41af-ab94-778f8f00b8af This call still returns html. And parsing the html is awkward without a tag identifying the code.

aru-py commented 10 months ago

@gasilvis How can we copy the code if it opens the user's browser? If we open our own browser, then users would have to login everytime.

dbenn commented 10 months ago

@gasilvis, re:

@aru-py Thanks for that json decode snippet. Saved me from having to use and external library.

Do you mean the Java function I wrote above? :)

dbenn commented 10 months ago

@aru-py Back for a couple of days so will work on the UI updates next.

gasilvis commented 10 months ago

I think I follow: If I don't open up a browser and I'm not logged in, the AAVSO has no way to pop up a login page. I tried sending the request as a GET ( curl -X GET -k -i 'https://apps.aavso.org/auth/external?app=sunentry&format=json&identifier=daf4204c-8126-41af-ab94-778f8f00b8af' ) and the html returned indicates that it wants to redirect to the login.

Would it be possible to send the above as a GET and if logged in, just return the credentials. If not logged in the app would have to send the request again from a browser so the login screen could be popped up.

I think it is awkward to have to manually enter a code every time I open the SunEntry app, even when the system already knows I'm logged in.

Is the issue that the system will know I am logged in by cookies the browser environment can read? Then I can see that a non-browser app has to do it this way.

aru-py commented 10 months ago

@gasilvis There's no way to send a GET request to the user's default browser and automatically get the credentials. That would pose a major security issue :). You should not have to login every time you open the app, only when the token (if you're using it) expires or the user logs out. It would be inconvenient if users would have to do this process every time. They should remain logged in.

gasilvis commented 10 months ago

When ever I open SunEntry I have to redo the code transfer. I missed the part of how to use the token. So, how do I use the token?

gasilvis commented 10 months ago

Ok. The token is for API access. Our API's are wide open, but someday we will need these tokens for API access.

@dbenn I understand from @aru-py that we can avoid the login process by simply saving and reusing the credentials we have received. For now it's up to our applications to decide when they should renew the Auth0 connection. Question: How is VStar going hold this credential information on the user's computer? I'm looking for a common strategy to use with the WebStart applications as I believe we have some 5 WebStart applications: SunEntry, SeqPlot, RemarkOMatic, Zapper and Zapper-Validator. INI files?

aru-py commented 10 months ago

@dbenn @gasilvis Checking in – is there anything I can help with? Do you guys have a timeline for when you would be ready for testing?

dbenn commented 10 months ago

@gasilvis Apologies for the delay. Busy last couple of days and interstate again for the next two days.

I'm storing everything from the POST response in the LoginInfo object showed in the code snippet above. That's pretty much the extent of it.

Like you, I don't have an immediate need for the token. I really just want membership status (for a few plugins) and observer code (for filtering operations and discrepant marking).

I'll be committing more code on the weekend when I get back. LoginInfo is already there and was previously used with the last auth mechanism.

Happy to talk more, here, in email or via Skype. Let me know if you have thoughts or questions.

dbenn commented 10 months ago

@aru-py Thanks for checking in and apologies for the sporadic interactions.

I wrote/modified the UI code and hooked it up to the new Auth0 authentication code handling class (which has been tested) including UUID generation. Started testing it last night then needed to finish up some work and prepare for travel this morning.

Fixed one problem in the UI code so far. I anticipate that initial testing will be complete by the end of Sunday my time (GST+10.5).

I will show some screenshots here then.

I imagine you will want to check logs to ensure REST API interactions are OK.

Was this how you wanted to proceed or do you have something else in mind?

Thanks.

aru-py commented 10 months ago

@dbenn Yes, that sounds good. Thank you.