diff --git a/CHANGELOG.md b/CHANGELOG.md index 74a094eb0..3792c1ada 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Changed * The string `repr` of `DatasetVersion` (e.g. `dataset.versions`) is now valid Julia code. ([#84]) +* `JuliaHub.authenticate` will now fall back to force-authentication if the token in an existing `auth.toml` file is found to be invalid during authentication. ([#86]) ### Fixed @@ -150,4 +151,5 @@ Initial package release. [#58]: https://github.com/JuliaComputing/JuliaHub.jl/issues/58 [#74]: https://github.com/JuliaComputing/JuliaHub.jl/issues/74 [#83]: https://github.com/JuliaComputing/JuliaHub.jl/issues/83 -[#84]: https://github.com/JuliaComputing/JuliaHub.jl/issues/84 \ No newline at end of file +[#84]: https://github.com/JuliaComputing/JuliaHub.jl/issues/84 +[#86]: https://github.com/JuliaComputing/JuliaHub.jl/issues/86 diff --git a/src/authentication.jl b/src/authentication.jl index 84e42c23c..cb879b761 100644 --- a/src/authentication.jl +++ b/src/authentication.jl @@ -198,10 +198,15 @@ as different authentication calls may clash. function authenticate end function authenticate(server::AbstractString, token::Union{AbstractString, Secret}) - auth = _authentication( - _juliahub_uri(server); - token=isa(token, Secret) ? token : Secret(token), - ) + auth = try + _authentication( + _juliahub_uri(server); + token=isa(token, Secret) ? token : Secret(token), + ) + catch e + isa(e, InvalidAuthentication) || rethrow() + throw(AuthenticationError("The authentication token is invalid")) + end global __AUTH__[] = auth return auth end @@ -259,7 +264,44 @@ function _authenticate( # _authenticate either returns a valid token, or throws auth_toml = _authenticate_retry(string(server_uri), 1; force, maxcount) # Note: _authentication may throw, which gets passed on to the user - _authentication(server_uri; auth_toml...) + try + _authentication(server_uri; auth_toml...) + catch e + # If the token in auth.toml is invalid, but it hasn't expired, + # PkgAuthentication won't catch that, and we attempt to use it (to get the + # API version etc). If the token is invalid, that fails with a 401 and + # _authentication() throws. In this case, we will go ahead and remove the token + # and try again (which should lead to the interactive authentication flow). + if !isa(e, InvalidAuthentication) || (maxcount <= 1) + rethrow() + end + # We'll back up the old auth.toml though, because the user did not ask + # us to remove it, so we don't want to delete the token for them either. + # To avoid overwriting an existing backup, we generate a unique name + # by hashing the file contents. + backup_path = string( + auth_toml.tokenpath, + ".", + bytes2hex(open(SHA.sha1, auth_toml.tokenpath))[1:8], + ".backup", + ) + mv(auth_toml.tokenpath, backup_path; force=true) + @warn """ + Existing token for $(server_uri) appears invalid; forcing reauthentication. + Existing auth.toml backed up in: $(backup_path) + """ + # We assume that _authenticate_retry immediately returned the token, + # and didn't retry multiple times. So we just bump `count` by one here. + auth_toml = _authenticate_retry(string(server_uri), 2; force=true, maxcount) + try + _authentication(server_uri; auth_toml...) + catch e + # If it again fails with InvalidAuthentication, we give up. But we + # need to throw AuthenticationError. + isa(e, InvalidAuthentication) || rethrow() + throw(AuthenticationError("JuliaHub returned an invalid authentication token")) + end + end finally isnothing(hook) || PkgAuthentication.clear_open_browser_hook() end @@ -340,6 +382,7 @@ function _authentication( api = try _get_api_information(string(server), token) catch e + isa(e, InvalidAuthentication) && rethrow() errmsg = """ Unable to determine JuliaHub API version. _get_api_information failed with an exception: diff --git a/test/authentication.jl b/test/authentication.jl index 3947eb169..0690cd8a7 100644 --- a/test/authentication.jl +++ b/test/authentication.jl @@ -30,7 +30,7 @@ end # In the general authenticate() tests, we mock the call to JuliaHub._authenticate() # So here we call a lower level JuliaHub._authenticat**ion** implementation, with # the REST calls mocked. -@testset "JuliaHub._authenticate()" begin +@testset "JuliaHub._authentication()" begin empty!(MOCK_JULIAHUB_STATE) server = URIs.URI("https://juliahub.example.org") token = JuliaHub.Secret("") @@ -172,5 +172,12 @@ end delete!(MOCK_JULIAHUB_STATE, :auth_v1_status) MOCK_JULIAHUB_STATE[:auth_v1_username] = nothing @test_throws JuliaHub.AuthenticationError JuliaHub.authenticate(server, token) + + # Test that we handle InvalidAuthentication correctly in _authentication() + empty!(MOCK_JULIAHUB_STATE) + MOCK_JULIAHUB_STATE[:auth_v1_status] = 401 + @test_throws JuliaHub.AuthenticationError( + "The authentication token is invalid" + ) JuliaHub.authenticate(server, token) end end