IdentityServer3 login loop for clients using different flows - identityserver3

Our IdentityServer3 services is acting oddly, when we try to login from a client using hybrid flow and then login from a different client using implicit flow. The second user is stuck in a loop where the STS fails to recognize the authorization and sends the user back to the login page repeatedly. We can recreate this behavior and see it in our log files. Also, which client is logged on first that can login successfully, while the second one gets stuck in the loop. We have implemented a custom login process using the hooks for IdentityServer3. Below is the code for the login process:
public class EmsUserService : UserServiceBase
{
private IEmsEntities _context
{
get
{
var container = DependencyResolver.Current;
var service = container.GetService<IEmsEntities>();
service.ChangeReadUncommittedSetting(true);
return service;
}
}
public override Task PreAuthenticateAsync(PreAuthenticationContext context)
{
var request = HttpContext.Current.Request;
if (HttpContext.Current.Request.IsAuthenticated)
{
var personId = HttpContext.Current.User.Identity.Name;
var user = _context.Person.Include(p => p.PersonLogin).First(x => x.PersonId.ToString() == personId);
context.AuthenticateResult = new AuthenticateResult(personId, user.PersonLogin.LoginName);
}
return Task.FromResult(0);
}
public override Task AuthenticateLocalAsync(LocalAuthenticationContext context)
{
var password = context.Password;
var encryptPassword = Password.Encrypt(password);
var allUserLoginRecords = Utilities.GetUserLoginRecords(_context, encryptPassword, context.UserName);
//We want to make sure that we recognize if a user already exists but is locked out. "Locked out" is determined by a username/password
//that matches but is not currently active
if (allUserLoginRecords.Any())
{
if (allUserLoginRecords.Any(u => u.PersonLogin.IsActive))
{
var active = allUserLoginRecords.First(u => u.PersonLogin.IsActive);
var isOld = Utilities.IsOld(active.PersonLogin);
var isComplex = Utilities.IsComplex(context.Password);
var resetRequired = active.PersonLogin.IsResetRequired;
if (isOld || !isComplex || resetRequired)
{
context.AuthenticateResult = new AuthenticateResult(String.Format("~/ChangePassword/{0}", isOld),
active.PersonId.ToString(),
active.PersonLogin.LoginName);
}
else
{
context.AuthenticateResult = new AuthenticateResult(active.PersonId.ToString(),
active.PersonLogin.LoginName);
}
}
else
{
context.AuthenticateResult =
new AuthenticateResult(
"This account has been locked. Please contact OSPI Customer Support to re-activate it.");
}
}
else
{
context.AuthenticateResult =
new AuthenticateResult(
"The username and password combination does not match. Your username must be an email address and the password is case-sensitive.");
}
return Task.FromResult(0);
}
public override Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var claims = new List<Claim>();
var subjectId = context.Subject.GetSubjectId();
// issue the claims for the users
var user = _context.Person.Include(p => p.PersonLogin).First(x => x.PersonId.ToString() == subjectId && x.PersonLogin.IsActive);
if (user != null)
{
var emailClaim = new Claim(ClaimTypes.Email, user.PersonLogin.LoginName);
claims.Add(emailClaim);
var nameClaim = new Claim(ClaimTypes.Name, user.PersonId.ToString());
claims.Add(nameClaim);
context.IssuedClaims = claims;
}
return Task.FromResult(0);
}
public override Task IsActiveAsync(IsActiveContext context)
{
var subjectId = context.Subject.GetSubjectId();
// Look up the user to make sure that they're active
var user = _context.Person.Include(p => p.PersonLogin).First(x => x.PersonId.ToString() == subjectId && x.PersonLogin.IsActive);
if (user == null)
{
context.IsActive = false;
return Task.FromResult(0);
}
context.IsActive = true;
return Task.FromResult(0);
}
}
Here are the IdentityServer logs for the second client trying to log in:
> w3wp.exe Information: 0 : 2016-09-29 10:07:08.008 -07:00 [Information] User is not authenticated. Redirecting to login.
w3wp.exe Information: 0 : 2016-09-29 10:07:08.009 -07:00 [Information] End authorize request
w3wp.exe Information: 0 : 2016-09-29 10:07:08.010 -07:00 [Information] Redirecting to login page
2016-09-29 10:07:08.011 -07:00 [Debug] Protecting message: "{\"ReturnUrl\":\"https://localhost/OspiSts/identity/connect/authorize?client_id=.....&redirect_uri=......&response_mode=form_post&response_type=code%20id_token%20token&scope=openid%20profile%20authApi&state=OpenIdConnect.AuthenticationProperties%.....\",\"ClientId\":\"....",\"AcrValues\":[],\"Created\":636107656279778454}"
w3wp.exe Information: 0 : 2016-09-29 10:07:08.018 -07:00 [Information] Login page requested
2016-09-29 10:07:08.019 -07:00 [Debug] signin message passed to login: "{
\"ReturnUrl\": \"......",
\"ClientId\": \"...",
\"IdP\": null,
\"Tenant\": null,
\"LoginHint\": null,
\"DisplayMode\": null,
\"UiLocales\": null,
\"AcrValues\": [],
\"Created\": 636107656279778454
}"
w3wp.exe Information: 0 : 2016-09-29 10:07:12.847 -07:00 [Information] rendering login page
w3wp.exe Information: 0 : 2016-09-29 10:07:21.148 -07:00 [Information] Login page submitted
w3wp.exe Information: 0 : 2016-09-29 10:07:27.417 -07:00 [Information] Login credentials successfully validated by user service
w3wp.exe Information: 0 : 2016-09-29 10:07:27.419 -07:00 [Information] {
"Category": "Authentication",
"Name": "Local Login Success",
"EventType": "Success",
"Id": 1010,
"Details": {
"LoginUserName": "...",
"SignInId": "...",
"SignInMessage": {
"ReturnUrl": "....",
"ClientId": "...",
"AcrValues": [],
"Created": 636107656279778454
},
"PartialLogin": false,
"SubjectId": "141480",
"Name": "..."
},
"Context": {
"ActivityId": "77206837-cc1c-475b-a3e0-9b362b9dad29",
"TimeStamp": "2016-09-29T17:07:27.4199277+00:00",
"ProcessId": 1712,
"MachineName": "WKS-C070421",
"RemoteIpAddress": "::1"
}
}
w3wp.exe Information: 0 : 2016-09-29 10:07:27.421 -07:00 [Information] Calling PostAuthenticateAsync on the user service
w3wp.exe Information: 0 : 2016-09-29 10:07:27.425 -07:00 [Information] issuing primary signin cookie
w3wp.exe Information: 0 : 2016-09-29 10:07:27.428 -07:00 [Information] redirecting to: ....
w3wp.exe Information: 0 : 2016-09-29 10:07:27.442 -07:00 [Information] Start authorize request
w3wp.exe Information: 0 : 2016-09-29 10:07:27.443 -07:00 [Information] Start authorize request protocol validation
w3wp.exe Information: 0 : 2016-09-29 10:07:27.479 -07:00 [Information] "Authorize request validation success"
"{
\"ClientId\": \"...",
\"ClientName\": \"Data Quality\",
\"RedirectUri\": \"...",
\"AllowedRedirectUris\": [
\"...",
\"....",
\"....",
\"...."
],
\"SubjectId\": \"unknown\",
\"ResponseType\": \"code id_token token\",
\"ResponseMode\": \"form_post\",
\"Flow\": \"Hybrid\",
\"RequestedScopes\": \"openid profile authApi\",
\"State\": \"OpenIdConnect.AuthenticationProperties=....",
\"Nonce\": \"..",
\"Raw\": {
\"client_id\": \"...",
\"redirect_uri\": \"...",
\"response_mode\": \"form_post\",
\"response_type\": \"code id_token token\",
\"scope\": \"openid profile authApi\",
\"state\": \"OpenIdConnect.AuthenticationProperties=...",
\"nonce\": \"..."
}
}"
w3wp.exe Information: 0 : 2016-09-29 10:07:27.480 -07:00 [Information] User is not authenticated. Redirecting to login.
w3wp.exe Information: 0 : 2016-09-29 10:07:27.481 -07:00 [Information] End authorize request
w3wp.exe Information: 0 : 2016-09-29 10:07:27.483 -07:00 [Information] Redirecting to login page
2016-09-29 10:07:27.484 -07:00 [Debug] Protecting message: "{\"ReturnUrl\":\"..",\"ClientId\":\"...",\"AcrValues\":[],\"Created\":636107656474419255}"
w3wp.exe Information: 0 : 2016-09-29 10:07:27.493 -07:00 [Information] Login page requested
2016-09-29 10:07:27.495 -07:00 [Debug] signin message passed to login: "{
\"ReturnUrl\": \"...",
\"ClientId\": \"...",
\"IdP\": null,
\"Tenant\": null,
\"LoginHint\": null,
\"DisplayMode\": null,
\"UiLocales\": null,
\"AcrValues\": [],
\"Created\": 636107656474419255
}"
w3wp.exe Information: 0 : 2016-09-29 10:07:28.657 -07:00 [Information] rendering login page
You can see that I am redirected to the login, I login in successfully, but the STS fails to recognize the authentication. This occurs for the second client when logging in regardless of which one that is.
Thanks in advance for the help.

There is a bug in Microsoft's Owin implementation for System.Web. The one that is being used when running Owin applications on IIS. Which is what probably 99% of us do, if we're using the new Owin-based authentication handling with ASP.NET MVC5.
The bug makes cookies set by Owin mysteriously disappear on some occasions.
This middleware is a fix for that bug. Simple add it before any cookie handling middleware and it will preserve the authentication cookies. Here is the link
https://github.com/KentorIT/owin-cookie-saver

Related

OpenAM - Getting a Session attribute into an OpenID Connect claim

I'm using OpenAM 13.5 configured to have a SAML circle of trust to federate logins to our applications with third-party IdPs. Some of the SAML assertions received by the third party are mapped as Session level attributes. The SAML part is working fine, but I need to connect to OpenAM an application who can talk OpenID Connect. I created an OpenID Connect service, configured the client accordingly and I can login successfully using the flow "App -> OpenAM UI -> 3rd party IDP -> OpenAM OIDC -> App".
The problem is that I can retrieve only the attributes that are mapped to the data store - the session attributes (e.g. AuthLevel, IDP Name, etc) aren't included in the mapped claims.
I tried to edit the OIDC Claims default script which has a session variable that seems to contain what I need, but unfortunately the session variable is always null.
Is this the correct approach? Why is the session null? Is there something I need to enable in order to read it?
Thanks in advance for your help.
You can not retrieve an SSO session property in OIDC claimscript because the OAuth2 client does not send the SSO tracking cookie in the token request.
It's only possible if you use AM proprietary feature 'always include claims in ID token'.
As described by Bernhard in the comments, once a request arrives to the /userinfo endpoint OpenAM has no way to reconcile the access token with a living session (and the session could not exist any more as well).
However when accessing the claims inside the ID Token by activating the proprietary AM feature "Always include claims in ID Token" the session object is available and we can poll its properties!
For future readers, this is how I modified the OIDC script:
/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2014-2016 ForgeRock AS.
*/
import com.iplanet.sso.SSOException
import com.sun.identity.idm.IdRepoException
import org.forgerock.oauth2.core.UserInfoClaims
/*
* Defined variables:
* logger - always presents, the "OAuth2Provider" debug logger instance
* claims - always present, default server provided claims
* session - present if the request contains the session cookie, the user's session object
* identity - always present, the identity of the resource owner
* scopes - always present, the requested scopes
* requestedClaims - Map<String, Set<String>>
* always present, not empty if the request contains a claims parameter and server has enabled
* claims_parameter_supported, map of requested claims to possible values, otherwise empty,
* requested claims with no requested values will have a key but no value in the map. A key with
* a single value in its Set indicates this is the only value that should be returned.
* Required to return a Map of claims to be added to the id_token claims
*
* Expected return value structure:
* UserInfoClaims {
* Map<String, Object> values; // The values of the claims for the user information
* Map<String, List<String>> compositeScopes; // Mapping of scope name to a list of claim names.
* }
*/
// user session not guaranteed to be present
boolean sessionPresent = session != null
def fromSet = { claim, attr ->
if (attr != null && attr.size() == 1){
attr.iterator().next()
} else if (attr != null && attr.size() > 1){
attr
} else if (logger.warningEnabled()) {
logger.warning("OpenAMScopeValidator.getUserInfo(): Got an empty result for claim=$claim");
}
}
attributeRetriever = { attribute, claim, identity, session, requested ->
if (requested == null || requested.isEmpty()) {
fromSet(claim, identity.getAttribute(attribute))
} else if (requested.size() == 1) {
requested.iterator().next()
} else {
throw new RuntimeException("No selection logic for $claim defined. Values: $requested")
}
}
sessionAttributeRetriever = { attribute, claim, identity, session, requested ->
if (requested == null || requested.isEmpty()) {
if (session != null) {
fromSet(claim, session.getProperty(attribute))
} else {
null
}
} else if (requested.size() == 1) {
requested.iterator().next()
} else {
throw new RuntimeException("No selection logic for $claim defined. Values: $requested")
}
}
// [ {claim}: {attribute retriever}, ... ]
claimAttributes = [
"email": attributeRetriever.curry("mail"),
"address": { claim, identity, session, requested -> [ "formatted" : attributeRetriever("postaladdress", claim, identity, session, requested) ] },
"phone_number": attributeRetriever.curry("telephonenumber"),
"given_name": attributeRetriever.curry("givenname"),
"zoneinfo": attributeRetriever.curry("preferredtimezone"),
"family_name": attributeRetriever.curry("sn"),
"locale": attributeRetriever.curry("preferredlocale"),
"name": attributeRetriever.curry("cn"),
"spid_uid": attributeRetriever.curry("employeeNumber"),
"spid_idp": attributeRetriever.curry("idpEntityId"),
"spid_gender": attributeRetriever.curry("description"),
"spid_authType": sessionAttributeRetriever.curry("AuthType"),
"spid_authLevel": sessionAttributeRetriever.curry("AuthLevel"),
]
// {scope}: [ {claim}, ... ]
scopeClaimsMap = [
"email": [ "email" ],
"address": [ "address" ],
"phone": [ "phone_number" ],
"profile": [ "given_name", "zoneinfo", "family_name", "locale", "name" ],
"spid": [ "spid_uid", "spid_idp", "spid_authType", "spid_authLevel", "spid_gender" ],
]
if (logger.messageEnabled()) {
scopes.findAll { s -> !("openid".equals(s) || scopeClaimsMap.containsKey(s)) }.each { s ->
logger.message("OpenAMScopeValidator.getUserInfo()::Message: scope not bound to claims: $s")
}
}
def computeClaim = { claim, requestedValues ->
try {
[ claim, claimAttributes.get(claim)(claim, identity, session, requestedValues) ]
} catch (IdRepoException e) {
if (logger.warningEnabled()) {
logger.warning("OpenAMScopeValidator.getUserInfo(): Unable to retrieve attribute=$attribute", e);
}
} catch (SSOException e) {
if (logger.warningEnabled()) {
logger.warning("OpenAMScopeValidator.getUserInfo(): Unable to retrieve attribute=$attribute", e);
}
}
}
def computedClaims = scopes.findAll { s -> !"openid".equals(s) && scopeClaimsMap.containsKey(s) }.inject(claims) { map, s ->
scopeClaims = scopeClaimsMap.get(s)
map << scopeClaims.findAll { c -> !requestedClaims.containsKey(c) }.collectEntries([:]) { claim -> computeClaim(claim, null) }
}.findAll { map -> map.value != null } << requestedClaims.collectEntries([:]) { claim, requestedValue ->
computeClaim(claim, requestedValue)
}
def compositeScopes = scopeClaimsMap.findAll { scope ->
scopes.contains(scope.key)
}
return new UserInfoClaims((Map)computedClaims, (Map)compositeScopes)
I also had to add the java.util.ArrayList$Itr class to the script classes whitelist.
Thanks for your help!

MarkLogic 9 permission issue on REST endpoint?

I have a function in a module for writing a user profile object.
// create scc user
function createUserProfile(userObj) {
// create user
var uri = fn.concat('/users/',userObj.id,'.json');
var data = {uri : uri, userName : userObj.userName, doc : userObj };
// we add the user as owner of this insert so he/she will own the doc
var options = {"isolation" : "different-transaction"}; //,"userId" :
xdmp.user(userObj.userName)};
xdmp.eval('declareUpdate();
xdmp.documentInsert(uri, doc,[xdmp.permission(userName,"read"),xdmp.permission(userName,"update")]);
xdmp.documentAddCollections(uri,"sccss-users")',
data,options);
return uri
};
Now if I first create the object the function works, but if I update the object using the same function it works from the console but not from a REST client?
Is there an issue with permissions? I have tried adding
var options = {"isolation" : "different-transaction","userId" : xdmp.user(userObj.userName)};
works again in console but not solved my issue.
Actually the REST client does not get an answer back from the endpoint, after a while de database times out, see the log.
The MarkLogic serverlog:
2018-01-25 21:28:13.597 Info: {"firstName":"Hugo", "lastName":"Koopmans", "userName":"xxxxxx", "email":"xxx#xxx.com", "password":"xxxxx", "activationCode":"4db600e771f85073a03ed16371ba39ad3b408852", "active":false, "id":"xxxxxx", "devices":["xxxxxxx"]}
2018-01-25 21:29:47.559 Notice: SVC-CANCELED: xdmp.documentInsert("/users/952187962xx.json", ObjectNode({"firstName":"Hugo", "lastName":"Koopmans", "userName":"xxxxx", "email":"...", ...}), Sequence({...XDMP-CANCELED: Canceled request "/v1/users/passwordreset"..., {...XDMP-CANCELED: Canceled request "/v1/users/passwordreset"...)) -- Canceled
2018-01-25 21:29:47.559 Notice:+in xdmp:eval("declareUpdate(); xdmp.documentInsert(uri, doc,[xdmp.permission(u...", {uri:"/users/952187962xxxxx.json", userName:"xxxxx", doc:{firstName:"Hugo", lastName:"Koopmans", userName:"xxxxx", ...}}, {isolation:"different-transaction", userId:"9521879xxxx"}) [javascript]
2018-01-25 21:29:47.559 Notice:+in /v1/users.sjs [javascript]
2018-01-25 21:29:47.559 Notice:+in /v1/users.sjs [javascript]
2018-01-25 21:29:48.535 Notice: XDMP-CANCELED: Canceled request "/MarkLogic/rest-api/error-handler.xqy"
2018-01-25 21:29:48.535 Notice:+in /MarkLogic/manage/endpoints/databases/partitions/partitions.xqy, at 184:9, in pa:get-partition() [1.0-ml]
2018-01-25 21:29:48.535 Notice: XDMP-CANCELED: Canceled request "/MarkLogic/rest-api/error-handler.xqy"
2018-01-25 21:29:48.535 Notice:+in /MarkLogic/rest-api/error-handler.xqy [1.0-ml]
Questions:
- The permissions 'read' an 'update' should do the trick here also for actually updating a document with xdmp.documentInsert()
- Why does the function work if run from the console (as admin but with userId set as explained above.

Invalid credentials when trying to authenticate with password in Realm

I'm trying to authenticate a user with password like this:
let syncCredentials = SyncCredentials.usernamePassword(username: email, password: password, register: true)
SyncUser.logIn(with: syncCredentials, server: Constants.Realm.Server) { (realmUser, error) in
guard let realmUser = realmUser else {
DDLogError("\(error)")
return
}
DDLogInfo("realmUser: \(realmUser)")
}
but it prints out this error:
Optional(Error Domain=io.realm.sync Code=611 "The provided credentials are invalid." UserInfo={statusCode=400, NSLocalizedDescription=The provided credentials are invalid.})
The Server constant is correct, as I can successfully connect to the Realm Object Server using Facebook credentials.
You can get this error if you register the user that already exists, so specify register: false if the user is already registered.

Yahoo gemini Get job status

Im following this link
i have successfully got this result:
{
"errors": null,
"response": {
"jobId": "628cf1c0cd1c5d8a6c1765f618b7a0be34c50bc1564618",
"status": "submitted",
"jobResponse": null
}
}
ant tried GET call to check the status of the job , exactly as they are asking
https://api.admanager.yahoo.com/v1/rest/reports/custom/628cf1c0cd1c5d8a6c1765f618b7a0be34c50bc1564618?access_token=&advertiserId=21
but i get an error massage :
<?xml version="1.0" encoding="UTF-8"?>
-
Please provide valid credentials. OAuth oauth_problem="unable_to_determine_oauth_type", realm="yahooapis.com"
what am i doing wrong ?
i have even tried adding the access token
Yahoo API wasn't that precise beside the fact they had some system maintenance
this is how the url for testing if job has finished:
String urlToCheckIfDownloadReady = "https://api.admanager.yahoo.com/v1/rest/reports/custom/" + jobId + "?access_token=" + refreshToken + "&advertiserId=" + config.mccCustomerId;
if status = completed
Get from the Json response the jobResponse and than download it

404 for confluence json-rpc call

I'm attempting to write against the json-rpc API for confluence (in java). However, I'm getting a 404. I know that I'm getting to the server and authenticating (Basic auth) via the logs:
2013-10-08 10:48:59,586 DEBUG [http-8090-2] [atlassian.seraph.filter.BaseLoginFilter] doFilter doFilter : ____ Attempting login for : '/confluence/rpc/json-rpc/confluenceservice-v2'
2013-10-08 10:48:59,586 DEBUG [http-8090-2] [atlassian.seraph.filter.PasswordBasedLoginFilter] login login : No user name or password was returned. No authentication attempt will be made. User may still be found via a SecurityFilter later.
2013-10-08 10:48:59,586 DEBUG [http-8090-2] [atlassian.seraph.filter.BaseLoginFilter] doFilter doFilter : Login completed for 'null' - os_authstatus = 'null'
2013-10-08 10:48:59,587 DEBUG [http-8090-2] [atlassian.seraph.filter.SecurityFilter] doFilter doFilter : Storing the originally requested URL (atlassian.core.seraph.original.url=/confluence/rpc/json-rpc/confluenceservice-v2)
(that's good, see the rpc call)
2013-10-07 16:25:22,580 DEBUG [http-8090-2] [atlassian.seraph.auth.DefaultAuthenticator] login login : 'george' has been authenticated
2013-10-07 16:25:22,582 DEBUG [http-8090-2] [atlassian.seraph.auth.DefaultAuthenticator] authoriseUserAndEstablishSession authoriseUser : 'george' can login according to the RoleMapper
2013-10-07 16:25:22,584 DEBUG [http-8090-2] [net.sf.hibernate.SQL] log update logininfo set CURFAILED=?, TOTALFAILED=?, SUCCESSDATE=?, PREVSUCCESSDATE=?, FAILEDDATE=?, USERNAME=? where id=?
2013-10-07 16:25:22,586 DEBUG [http-8090-2] [atlassian.seraph.auth.DefaultAuthenticator] getUserFromBasicAuthentication getUserFromSession : Authenticated 'george' via Basic Auth
2013-10-07 16:25:22,587 DEBUG [http-8090-2] [atlassian.seraph.filter.SecurityFilter] doFilter doFilter : Setting Auth Context to be 'george'
But the response to a post to the URL
http://my.confluence.host.com:8090/confluence/rpc/json-rpc/confluenceservice-v2
with a body of
"{ \"jsonrpc\" : \"2.0\", \"method\" : \"getServerInfo\", \"id\" : 12345}"
returns with a 404.
The next log entry is
2013-10-08 10:48:59,626 DEBUG [http-8090-2] [atlassian.seraph.filter.SecurityFilter] doFilter doFilter : Storing the originally requested URL (atlassian.core.seraph.original.url=/fourohfour.action)
Plugin is enabled.
Thanks for any help!
The code:
#Test
public void test() {
RestTemplate rt = new TestRestTemplate();
String url = "http://my.confluence.host.com:8090/confluence/rpc/json-rpc/confluenceservice-v2";
rt.getMessageConverters().add(new MappingJacksonHttpMessageConverter());
rt.getMessageConverters().add(new StringHttpMessageConverter());
String body = "{ \"jsonrpc\" : \"2.0\", \"method\" : \"getServerInfo\", \"id\" : 12345}";
String returnValue = rt.postForObject(url, body, String.class);
}
private static final class TestRestTemplate extends RestTemplate {
#Override
protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException {
ClientHttpRequest request = super.createRequest(url, method);
String authentication = Base64.encode("george:password".getBytes());
request.getHeaders().add("Authorization", "Basic " + authentication);
return request;
}
}
Turns out the #(&*$ documentation was wrong and the reason why I was getting a 404 was because the url was not
http://host:port/confluence/rpc/json-rpc/confluenceservice-v2
but
http://host:port/rpc/json-rpc/confluenceservice-v2
So.. I was getting a 404 because... it was a 404. Code works great except that I had to add a content-type header for application/json.