Tuesday, September 24, 2013

CAS: Sample Client for Java

As the client for Java is missing at the jasig homepage I'll provide my solution here. Of course it has the "Domain" addition that I described in my earlier posts, but it is simple and quite obvious how to remove it.

So, here it is:

package cas;

import java.io.IOException;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.PostMethod;

public final class Client {
private static final Logger LOG = Logger.getLogger(Client.class.getName());

private Client() {
// static-only access
}

public static String getTicket(final String server, final String domain, final String username,
final String password, final String service) {
notNull(server, "server must not be null");
notNull(domain, "domain must not be null");
notNull(username, "username must not be null");
notNull(password, "password must not be null");
notNull(service, "service must not be null");

return getServiceTicket(server, getTicketGrantingTicket(server, domain, username, password), service);
}

private static String getServiceTicket(final String server, final String ticketGrantingTicket, final String service) {
if (ticketGrantingTicket == null)
return null;
final HttpClient client = new HttpClient();
final PostMethod post = new PostMethod(server + "/" + ticketGrantingTicket);
post.setRequestBody(new NameValuePair[] { new NameValuePair("service", service) });
try {
client.executeMethod(post);
final String response = post.getResponseBodyAsString();
switch (post.getStatusCode()) {
case 200:
return response;
default:
LOG.warning("Invalid response code (" + post.getStatusCode() + ") from CAS server!");
LOG.info("Response (1k): " + response.substring(0, Math.min(1024, response.length())));
break;
}
}
catch (final IOException e) {
LOG.warning(e.getMessage());
}
finally {
post.releaseConnection();
}
return null;
}

private static String getTicketGrantingTicket(final String server, final String domain, final String username,
final String password) {
final HttpClient client = new HttpClient();
final PostMethod post = new PostMethod(server);
post.setRequestBody(new NameValuePair[] { new NameValuePair("domain", domain),
new NameValuePair("username", username), new NameValuePair("password", password) });
try {
client.executeMethod(post);
final String response = post.getResponseBodyAsString();
switch (post.getStatusCode()) {
case 201: {
final Matcher matcher = Pattern.compile(".*action=\".*/(.*?)\".*").matcher(response);

if (matcher.matches())
return matcher.group(1);

LOG.warning("Successful ticket granting request, but no ticket found!");
LOG.info("Response (1k): " + response.substring(0, Math.min(1024, response.length())));
break;
}
default:
LOG.warning("Invalid response code (" + post.getStatusCode() + ") from CAS server!");
LOG.info("Response (1k): " + response.substring(0, Math.min(1024, response.length())));
break;
}
}
catch (final IOException e) {
LOG.warning(e.getMessage());
}
finally {
post.releaseConnection();
}
return null;
}

private static void notNull(final Object object, final String message) {
if (object == null)
throw new IllegalArgumentException(message);
}

public static void main(final String[] args) {
final String server = "https://login.myServer.org/v1/tickets";
final String domain = "www.myserver.org";
final String username = "JohnDoe";
final String password = "mySecret";

final String service = "https://www.myServiceServer.org/";

LOG.info(getTicket(server, domain, username, password, service));
}
}

btw: I have implemented the same procedure for iOS and put it on GitHub

Wednesday, September 18, 2013

Install CocoaPods on OSX-Mavericks (DP8)

Today I tried to upgrade CocoaPods in order to prepare my APPs to the upcoming changes in iOS7. Unfortunately I got an error

ERROR:  Error installing cocoapods:
ERROR: Failed to build gem native extension.


    /System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/ruby extconf.rb

I'm currently using "xCode5 GM" which is using the "OSX 10.8 SDK" which apparently is delivered without ruby version 2.0.

The solution was to download "xCode5 DP6" (containing the SDK 10.9), switch the command line tools to the version supplied by this release ("xcode-select") and run the following commands as a shell script

sys_rb_usr=/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr
sdk_rb_usr=`xcode-select -p`/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.9.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr

sudo cp -r $sdk_rb_usr/include $sys_rb_usr/include

And then update CocoaPods.

Wednesday, July 03, 2013

TestflightSDK - Beta testing for iOS and Android

We now introduced a new Testing system for our APPs in our company: Testflight

This system allows us to distribute our BETAs and RCs to our dedicated testers without bothering them any time we have a new version to come to us, connect the device and so on.

Beside this very useful function we can embed the TestFlightSDK directly in our APPs to monitor how the user is actually moving in the APP (right now only for iOS, but Android is coming soon)

Install the SDK
That's not very tricky... One way would be to download it and add it to your xCode, or the way I prefer is using Cocoapods to automatically inject it to my project.

As we only need it during development time we don't want to add the "Testflight.h" header in every single class where we use the monitoring. So we inject it using the "", and not in any case, but only during development

#ifdef __OBJC__
    #import
    #import
    #ifdef TESTFLIGHT
        #import
    #endif
#endif

Now we can enable it by going to the targets build settings and add a "-DTESTFLIGHT" to the "other C Flags" in the section "Apple LLVM compiler 4.2 - Language"

Activate it
That's quite simple too. Just go to your AppDelegate and add the following lines

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
#ifdef TESTFLIGHT
    // !!!: Use the next line only during beta
    [TestFlight setDeviceIdentifier:[[[UIDevice currentDevice] identifierForVendor] UUIDString]];
    [TestFlight takeOff:@""];
#endif
    return YES;
}

And the basic features are already activated.

You see we are again using the makro to remove TestFlightSDK on release time

Checkpoints
Nothing simpler and useful than that. Basically with this function you can check if users are passing points in the code. Set a Checkpoint as follows

#ifdef TESTFLIGHT
    [TestFlight passCheckpoint:@""];
#endif

In your TestFlight console you'll get a notification whenever a user passes this point in the execution

Feedback
Nice feature, even though the standard form is not usable in most cases. Fortunately they offer the possibility to use a custom form and just send the text to them (or better to your page on their website).

There are still a couple of interesting features in this SDK like questionnaires and remote logging that are worth to explore, and as soon as I had the time to do that I'll post them here.

Update March 2014:
TestFlight has been bought by Apple, and one of the first things they did was to announce that the Android support will be terminated by march 21th :-(

Friday, June 28, 2013

UIWebView detect scrolling

Today I'm trying to show and hide a View depending on the scrolling direction of a UIWebView.
Unfortunately just setting the delegate of the UIWebView in the storyboard (or in the code) is not enough  to get the method

- (void)scrollViewDidScroll:(UIScrollView *)scrollView

called.

That's because its not the delegate of the UIWebView that you want in this case but the delegate of its containing UIScrollview. So in your init method just set the following and you're done.

[self.eventUIWebView.scrollView setDelegate:self];

Friday, June 14, 2013

iOS7 - Beta

I know that it's a beta, and therefore bugs are present, but in some way we as developers are forced to test it and report those back to Apple in order for them to fix the issues.

So, here is my list of bug and unwanted behaviours

1) A strange thing that i found is in the Photo-APP. I made some pictures to see them in the APP. Then I tried to delete the picture before the last one, but the APP always deletes the last picture. This doesn't seem to happen by deleting arbitrary pics, but only the pre-last. (Fixed in Beta 3)

2) In the Contacts-APP the list of initial letters (right side of the screen) doesn't seem to be aligned correctly

3) Skype... isn't working at all

4) Don't put a picture with a light background in the lock screen. It just disables you from seeing the controls

5) In some Apps (for me the synology apps) that use the popup to ask for the credentials, there are just no inputfields. This means that there is no space to enter them and you're basically not able to login

6) Now in the latest Beta 5 you'll see all the birthdays one day earlier than they really are. My contacts APP is telling me that my birthday is the 18. of September, but in the Calendar I see it on the right date. So by restoring it for displaying in the contacts they'll make some kind of mistake

I'll put more of those as soon as I find them.

All in all I like the new look and feel of the OS and even the new functions are nice, however there is still a lot of work for Apple until they can distribute it to the market.

Wednesday, June 12, 2013

Mac OSX Mavericks - Issues

Since yesterday I'm now using the new Beta of OSX-Mavericks an I'm quite happy with it.

The good things:

  • Now you can have 2 full screen apps on two screens
  • You have the Maps App on OSX
  • Auto-Update in the App store
  • You can answer iMessages directly from the notifications


The bad things:

  • Skype quit to work (This can be fixed by moving to an older version)
  • XMarks for Safari does not work
  • Safari in general seems to have some issues. If you fast switch between Safari and other Apps after some time, and even not really reproducible Safari just doesn't react when u enter a new URL. After a Safari restart everything's fine again
  • I've a Problem getting the Xcode Command line tools working; Apparently I miss the ruby.h file - Found a fix for that one


I'll keep you updated if I find some more issues

Tuesday, April 09, 2013

iOS - use the keychain to store your passwords

Now that we are about to write APPs that access sensitive user related data we need to find a way how to store our username and password or in our case the CAS tickets in a secure storage such as the keychain on the mac.

Fortunately Apple provides access to the local keychain on the device and furthermore they provide a sample code how this could be done, but it is not yet ARC-compatible. After some googling I found a "corrected" version on GitHub which I use now in my project.

Using this class storing something in the keychain is quite easy:
KeychainItemWrapper *keychainItem = [[KeychainItemWrapper alloc] initWithIdentifier:@"MyAppLogin" accessGroup:nil];
[keychainItem setObject:@"Password" forKey:(__bridge id)(kSecValueData)];
[keychainItem setObject:@"Username" forKey:(__bridge id)(kSecAttrAccount)];
and so is reading it.
NSString *password = [keychainItem objectForKey:(__bridge id)(kSecValueData)];
NSString *username = [keychainItem objectForKey:(__bridge id)(kSecAttrAccount)];
If you don't need the information stored anymore or it is expired do some cleanup
[keychainItem resetKeychainItem];
and that's all.

Tuesday, February 12, 2013

CAS - nested group memberships

In order to do the authorisation on the application side the single application needs to know to which groups the user is assigned. In active directory (AD) this groups could be nested, therefore the query could be fairly difficult.

Fortunately the query at the end was not that difficult... at least if you know what you are searching for :-)

member:1.2.840.113556.1.4.1941:={0} 
//where {0} needs to be replaced by distinguishedName in our case

Now we need to integrate this query into our jasig CAS system. The main idea how this can be done comes from this blog, so if you need some more details try to read the post there. The main idea there is to use the spring-securitys DefaultLdapAuthoritiesPopulator to fetch the Roles and fill them into an authorities-list. So I modified it's definition to use our query and some other customisations to fit into my deployerConfigContext.xml

<bean id="ldapAuthoritiesPopulator"
class="org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator">
<constructor-arg ref="contextSource" /> 
<constructor-arg value="${ldap.searchBase}" /> 
<property name="groupRoleAttribute" value="cn" />
<property name="groupSearchFilter" value="(member:1.2.840.113556.1.4.1941:={0})" />
<property name="IgnorePartialResultException" value="true" />
<property name="searchSubtree" value="true" />
<property name="rolePrefix" value="" />
<property name="convertToUpperCase" value="false" />
</bean>

So now we need to put this new bean into relation with the others in order to make use of it. As we are working on getting more attributes we need to add it to the bean that's in charge of retrieving all the attributes.

<bean id="attributeRepository"
class="org.jasig.services.persondir.support.ldap.LdapPersonAttributeAndRoleDao">
<property name="ldapTemplate" ref="ldapTemplate" />
<property name="queryTemplate" value="{0}"/>
<property name="baseDN" value="${ldap.searchBase}" />
<property name="requireAllQueryAttributes" value="false" />
<property name="queryAttributeMapping">
<map>
<entry key="username" value="${ldap.username}" />
</map>
</property>
<property name="resultAttributeMapping">
<map>
<entry key="distinguishedName" value="distinguishedName" />
<entry key="member" value="member" />
</map>
</property>
<property name="ldapAuthoritiesPopulator" ref="ldapAuthoritiesPopulator" />
</bean>

That's quite a straight forward definition, except that the used class is not existing, so we need to define it. Basically its just similar to the one suggested in the other blog, with small modification to fit into our system. (The following just contains the modified method)

public class LdapPersonAttributeAndRoleDao extends LdapPersonAttributeDao {

private DefaultLdapAuthoritiesPopulator ldapAuthoritiesPopulator;

private String groupAttributeName = "member";

@Override
protected List getPeopleForQuery(LogicalFilterWrapper queryBuilder, String queryUserName) {
List attribs = super.getPeopleForQuery(queryBuilder, queryUserName);
final List peopleWithRoles = new ArrayList(attribs.size());
Collection authorities = null;
try {
IPersonAttributes person = attribs.get(0);
if (person.getAttributes().get("distinguishedName") != null) {
authorities = ldapAuthoritiesPopulator.getGrantedAuthorities(new DirContextAdapter((String) person
.getAttributes().get("distinguishedName").get(0)), queryUserName);
}
} catch (Exception nnfe) {
logger.error("error looking up authorities", nnfe);
}
List authoritiesList;
if (null != authorities && authorities.size() > 0) {
authoritiesList = new ArrayList();
for (GrantedAuthority auth : authorities) {
authoritiesList.add(auth);
}
for (IPersonAttributes person : attribs) {
Map> attrs = new HashMap>();
attrs.putAll(person.getAttributes());
attrs.put(getGroupAttributeName(), authoritiesList);
peopleWithRoles.add(new CaseInsensitiveAttributeNamedPersonImpl(this.getConfiguredUserNameAttribute(),
attrs));
}
} else {
peopleWithRoles.addAll(attribs);
}
return peopleWithRoles;
}
}

And that's it basically... but as the casServiceValidationSuccess.jsp mentioned in the blog looks simpler than my proposed solution I changed it to that one.

<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationSuccess>
<cas:user>${fn:escapeXml(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.id)}</cas:user>
<c:if test="${not empty pgtIou}">
<cas:proxyGrantingTicket>${pgtIou}</cas:proxyGrantingTicket>
</c:if>
<c:if test="${fn:length(assertion.chainedAuthentications) > 1}">
<cas:proxies>
<c:forEach var="proxy" items="${assertion.chainedAuthentications}"
varStatus="loopStatus" begin="0"
end="${fn:length(assertion.chainedAuthentications)-2}" step="1">
<cas:proxy>${fn:escapeXml(proxy.principal.id)}</cas:proxy>
</c:forEach>
</cas:proxies>
</c:if>
<c:if test="${fn:length(assertion.chainedAuthentications) > 0}">
<cas:attributes>
<c:forEach var="auth" items="${assertion.chainedAuthentications}">
<c:forEach var="attr" items="${auth.principal.attributes}" >
<cas:${fn:escapeXml(attr.key)}>${fn:escapeXml(attr.value)}</cas:${fn:escapeXml(attr.key)}>
</c:forEach>
</c:forEach>
</cas:attributes>
</c:if>
</cas:authenticationSuccess>
</cas:serviceResponse>

(That's of course not the first modification we implemented into our CAS, so if you don't understand some of the code try to read my other posts, or drop me a comment)

Thursday, January 24, 2013

Add a custom Tab/App to your Facebook Page


Lately we got the task to create a custom Facebook Fan page for our network. It should contain the most important facts about us and be always up to date. Actually nobody in our company has really the time to deal with a fan page, so we had to include our old homepage or at least parts of it. This is possible using custom tabs, which is not really difficult, but as I guess I will need it more often now I want to share my process in a few steps.

Before starting with the tutorial you'll need to create a Facebook page, then you can start with the following steps which show you how to add a tab to your facebook page
  1. Create a Webpage that contains the content you'd like to display on your custom tab
  2. Ensure that this page is accessible under http and https (Facebook requirement)
  3. Go to https://developers.facebook.com/apps
  4. Create a new App; set the Name, contact email, ...
  5. On the App's detail page there is a section called "Select how your app integrates with Facebook"; Open the "Page Tab" and put the Pages name and the two addresses (http and https) (*)
  6. Go to https://www.facebook.com/dialog/pagetab?next=http://facebook.com&app_id=XYZ (exchange XYZ with your actual AppID) and select to which of your Facebook pages you want to add the new Tab-App
  7. Go to the Facebook page and watch your result
(*) We noticed that the URLs are not allowed to contain commas or semicolon, so you need to encode them first ("," = %2C and ";" = %3B)

The best and shortest tutorial I could find on this topic was the following Youtube video
http://www.youtube.com/watch?v=SOpXDxrUm84

Thursday, January 10, 2013

CAS - Login from iOS

The CAS system is designed in a way that the administrator of the server needs to provide a list of services (URLs) that are allowed to use the CAS. This means, that by login in you'll get a TicketGrantingTicket (TGT) which can then be used to create ServiceTickets (ST) to authenticate to a specific service.

This causes some problems when you try to authenticate an APP on a cellular phone which usually does not have a fixed name or IP which could be registered on the CAS server. However, CAS offers the possibility to allow the generation of TGT and ST via a RESTful interface.

As you have probably seen in my previous posts our CAS comes with a set of customisations to fit into our needs of which the integration of a login-domain is probably the most complicated. Introducing such a new, third field for the authentication causes changes throughout the code and as it is needed now to generate TGTs and STs it needs to be implemented in the RESTful interface as well (see my link above for details)

The general usage of CAS's RESTful interface is stated in the CAS documentation, so I'm not going to copy it to this blog, but the interesting thing is its implementation into an iOS application which is not present there.

The general idea on how it can be done comes of course from the CAS documentation and by adapting this article about how to connect Drupal, CAS and iOS. My solution is partially copied from there introducing a newer more powerful API for consuming the RESTful WS and our new Domain field.

Actually the project does just the creation of the TGT and an ST for a sample service and returns the result after executing the "serviceValidate". This means that after this step we know on the iOS-APP exactly who logged in and all other attributes that are provided by the CAS.

What's still missing is a general solution on how this iOS-APP could authenticate itself now to an arbitrary web server like DRUPAL in the post stated above.

The sample project is hosted on github