Skip to content
This repository has been archived by the owner on May 27, 2019. It is now read-only.

Add feature to launch URLs & support basic auth #224

Merged
merged 10 commits into from
Mar 21, 2018

Conversation

erayd
Copy link
Contributor

@erayd erayd commented Mar 20, 2018

  • Host app will now return a "url" field
  • Browser plugin has a "launch URL" button for each entry
  • Browser plugin has a new hotkey (g) to launch the URL
  • Supply basic auth credentials if necessary when launching via the extension

Note that basic auth will only work in Firefox >= 54. Older versions don't have the required API to do this.

Closes #214.
Closes #103.

@maximbaz
Copy link
Member

maximbaz commented Mar 20, 2018

Nice! Let me throw some ideas first.

  1. Could you please check this out? When we are opening a new tab, can we try to additionally override onAuthRequired and prefill the user credentials, so that user is already logged in? As a bonus, this will also solve Support HTTP basic authentication (browser pop-up window) #103.

  2. When host app does not return a url, instead of just saying "sorry we can't do it", can we open the url of the entry name itself, if it looks like a domain? In other words, if my file name is google.com.gpg, I'd like browserpass to open google without having to add url: https://google.com to the file. Assuming the majority of password entries are named as a valid domain, it's not nice to render the button to open URL, when it cannot actually open the URL (unless I add this url: metadata to each and every password entry).

@erayd
Copy link
Contributor Author

erayd commented Mar 20, 2018

@maximbaz

  1. Will take a look and see what I can do.

  2. I will add this. Note that this will require bundling a TLD list with the extension - simply guessing which path component is the URL doesn't feel like a good idea (because it could result in things like http://work or http://myUsername).

@maximbaz
Copy link
Member

Thanks! Hmm, you are right, it is not very clear how to select the domain part from the entry name...

Although I wouldn't suggest building a super comprehensive domain matcher, because the workaround is very simple (adding url: to the password entry) - it should just work in most common cases.

@erayd
Copy link
Contributor Author

erayd commented Mar 21, 2018

@maximbaz I've implemented extracting a URL from the entry path. What do you think?

@erayd erayd changed the title Add feature to launch URLs if available Add feature to launch URLs & support basic auth Mar 21, 2018
@erayd
Copy link
Contributor Author

erayd commented Mar 21, 2018

@maximbaz HTTP basic auth now works too :-).

Copy link
Member

@maximbaz maximbaz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice work! I left some notes and questions.

}
});
// only supply credentials if this is the first time for this tab
if (!authAttempted) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you walk me through this logic? So we register the listener immediately after a tab is created, remove the listener after the tab is loaded (is it guaranteed that info.status will become complete only after basic auth is filled in?), and at the same time you additionally prevent supplying credentials more than once (why is this needed? won't removing the listener suffice?).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Create a new tab, and get the tab ID in the callback
  2. Add onAuthRequired listener
  3. When the auth callback fires, add a status listener to the tab
  4. Supply login credentials if we haven't already supplied them
  5. When the status listener fires and the status is complete (i.e. the page has loaded), remove both listeners.

The reason both the authCompleted and the listeners are required:

  • The onAuthRequired listener cannot remove itself, as doing so crashes the extension.
  • Because it cannot remove itself, it may fire multiple times (e.g. if the password is incorrect)
  • So the boolean restricts it to one time only
  • And removing it in the status listener once the page is complete ensures that the event is no longer live, and avoids a crash.

is it guaranteed that info.status will become complete only after basic auth is filled in?

I could not find any specific documentation guaranteeing this, and I looked pretty hard. However, the documentation does say that this occurs when the page load is complete, and blocking authentication requirements prevent the page from loading until they are satisfied. I've tested it extensively, and I'm comfortable that this does seem to be the case, but I cannot absolutely guarantee that it is so.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you, makes sense!

// only supply credentials if this is the first time for this tab
if (!authAttempted) {
authAttempted = true;
// ask the user before sending credentials over an insecure connection
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

}
},
{urls: ["*://*/*"], tabId: tab.id},
["blocking"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the guarantee that info.status will become complete only after basic auth is filled in? Or what is this for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not possible to mess with the request asynchronously (including providing authentication details). The "blocking" option ensures that the request waits for our callback to complete before proceeding.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, thanks!

@@ -7,7 +7,7 @@
"author": "Danny van Kooten",
"homepage_url": "https://github.com/dannyvankooten/browserpass",
"background": {
"persistent": false,
"persistent": true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you comment on why this is needed?

Copy link
Contributor Author

@erayd erayd Mar 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chrome doesn't allow non-persistent event pages to use the webRequest API. If you have this set to false, you'll get a massive warning when you try to load the extension telling you that this isn't possible, and then Chrome will refuse to load the extension.

function(response) {
if (chrome.runtime.lastError) {
error = chrome.runtime.lastError.message;
m.redraw();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: could you please reduce the nesting?
It's a little difficult to see which "else" belongs to which "if" 🙂

if (error) {
  ...
  return; 
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure.

} else {
// get url from login path if not available in the host app response
if (!response.hasOwnProperty("url") || response.url.length == 0) {
var parts = entry.split(/\//).reverse();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see pros and cons in reversing the parts, and so far I found only one case among my passwords where this is not ideal. We will probably keep this as it is, but I want to ask anyway, what is your reasoning for and against reversing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems more likely that someone will have a workplace that is a valid domain (e.g. "I work for example.com"), than a username that is a valid domain. It also seems more likely that if domains are nested, the deepest one is the most likely to be what the user wants. So reversing it prioritises domains that are found closer to the end of the entry.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed

if (response.hasOwnProperty("url") && response.url.length > 0) {
var url = response.url.match(/^([a-z]+:)?\/\//i) ? response.url : "http://" + response.url;
chrome.runtime.sendMessage({action: "launch", url: url, username: response.u, password: response.p});
window.close();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here's another potentially good place to do return and reduce nesting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

} else {
// no url available
if (!response.hasOwnProperty("url")) {
resetWithError("Your host application is too old - must be at least 2.0.14 for URL launch.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First brick in building #164, I like this 👍

However, we need to adjust the message, because with added parsing of domain it's not consistent anymore. With the old host application I can open url for password entries like "personal/google.com" just fine, until one day I try to open url for the entry "personal/notes" and suddenly get the error "Your host application is too old".

I think we should say something like this:

Unable to detect the URL to open for this entry. If you defined it in the password file, update your host application to the latest version.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point - done.

if (!response.hasOwnProperty("url")) {
resetWithError("Your host application is too old - must be at least 2.0.14 for URL launch.");
} else {
resetWithError("No URL is available for this login.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my mind something like this will sound better, maybe there is a URL but it's just us who cannot detect it:

Unable to detect the URL to open for this entry.

@maximbaz
Copy link
Member

maximbaz commented Mar 21, 2018

Attaching the compiled extension for the current code for others to test:

chrome-browserpass.crx.zip

firefox.zip

chrome.webRequest.onAuthRequired.addListener(
function authListener(requestDetails) {
// only supply credentials if this is the first time for this tab
if (!authAttempted) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you invert this too, i.e. if (authAttempted) { return {}; }? Also judging by line 123 returning {} is important in this case too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem - done :-).

Copy link
Member

@maximbaz maximbaz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks once again, this is awesome! I wanted to get some additional feedback from people in #103, but we can as well make a release and wait for people to submit issues if they experience anything weird 😉

@maximbaz
Copy link
Member

Just as a final confirmation, do you want to do anything else here or can I merge?

@erayd
Copy link
Contributor Author

erayd commented Mar 21, 2018

Thanks once again, this is awesome!

Most welcome :-).

Just as a final confirmation, do you want to do anything else here or can I merge?

If you could give me two minutes, I want to tweak the 'insecure connection' error message slightly. Stand by.

@erayd
Copy link
Contributor Author

erayd commented Mar 21, 2018

@maximbaz Thanks - I just wanted to clarify that message a bit; I figured the original wording wasn't the clearest.

I'm happy with it now - all good to merge :-). Thanks very much for your help with this, and for your extremely speedy code reviews!

@maximbaz maximbaz merged commit 604f560 into browserpass:master Mar 21, 2018
@erayd erayd deleted the launch-url branch March 21, 2018 21:17
authAttempted = true;
// remove event listeners once tab loading is complete
chrome.tabs.onUpdated.addListener(function statusListener(tabId, info) {
if (info.status === "complete") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@erayd do you think this code should also compare tabId with the tab.id before removing the listeners?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't need to, because the listeners are specific to the tab anyway.

There is a security problem, but that's not it. I'm filing an issue now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure that this doesn't register a listener that fires for all tabs? Maybe it's just a weird API, but the tab object is not used when registering the listener, and the tabId is provided as a first argument for a reason, both of these lead me to think that this will fire for all tabs...

I know that this is not about the security problem, it's just what I accidentally stumbled upon 🙂

Copy link
Contributor Author

@erayd erayd Mar 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh, good point. The onAuthRequested handler is specific to the tab, but the status listener is not. My bad.

I'm going to roll the security fix PR right now - I feel quite bad I didn't notice that in the first place; I'm normally more careful than that. I will fix this at the same time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@uri
Copy link
Contributor

uri commented Mar 22, 2018

Aside from this feature addressing the basic auth issue #103, what is the purpose of this? It seems strange to have this on all pass entries. Personally I don't have a single basic auth entry, so for me the button is a worse way to navigate to url? Is there another common use case for this that I'm missing?

@maximbaz
Copy link
Member

See #214 for more details, people seem to like this way of opening websites in Lastpass and other password managers, and they carry their habits to browserpass.

In addition it now allows you to use url: metadata in your pass files. For example, you might have a file called github.com.gpg, but inside of it you add url: https://github.com/login, and thus when you open github via browserpass, it opens the login form if you are unauthorized, or the home page if you are already authorized.

@uri
Copy link
Contributor

uri commented Mar 23, 2018

In addition it now allows you to use url:

Oh now that is useful.

#235

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants