First steps to an Erlang OpenID consumer

I’m working on a little project in Erlang and I wanted to use only OpenID for my authentication. It turns out there is currently no Erlang OpenID consumer library (or if there is, I couldn’t find it).

So I’ve started writing my own. So far I’ve got the first necessary step complete: HTML-based discovery.

I’m starting with version 1.1, simply because it is shorter and requires less (I don’t want to implement the XRI or Yadis protocols just yet).

It turns out that mochiweb comes with an HTML parser, so I used that, since I’m using mochiweb for my application. The parsed HTML comes back as a series of nested tuples of the format {<<“tag”>>, Attributes, Children}, where “tag” is the tagname (the root will be <<“html”>>, for example), Attributes is a proplist of that tag’s attributes, and Children is a list of more tuples of the same format and/or binaries with the contents of text nodes. Everything is represented as binaries, so I use those directly rather than converting between strings and binaries.

Here’s the code which finds the link tags with rel=“openid.server” and rel=“openid.delegate” (if it is there):

    get_openid_server(Identifier) ->
        NormalizedIdentifier = normalize_identifier(Identifier),
        case http:request(NormalizedIdentifier) of
            {ok, {_Status, _Headers, Body{% templatetag closevariable %} ->
                HtmlTokens = mochiweb_html:parse(Body),
                find_openid_tags(HtmlTokens);
            _ ->
                {error, http_error}
        end.
    
    normalize_identifier(Ident = "http://" ++ _Rest) ->
        Ident;
    normalize_identifier(Ident) ->
        "http://" ++ Ident.
    
    find_openid_tags(HtmlTokens) ->
        case find_tag(<<"head">>, [HtmlTokens]) of
            {<<"head">>, _Attrs, Children, _Rest} ->
                case find_tag_with_attr(<<"link">>, {<<"rel">>, <<"openid.server">>}, Children) of
                    not_found ->
                        {error, openid_server_not_found};
                    ServerAttrs ->
                        Server = proplists:get_value(<<"href">>, ServerAttrs),
                        case find_tag_with_attr(<<"link">>, {<<"rel">>, <<"openid.delegate">>}, Children) of
                            not_found ->
                                [{server, Server}];
                            DelegateAttrs ->
                                Delegate = proplists:get_value(<<"href">>, DelegateAttrs),
                                [{server, Server}, {delegate, Delegate}]
                        end
                end;
            not_found ->
                {error, no_head_tag}
        end.
    
    find_tag(_TagName, []) ->
        not_found;
    find_tag(TagName, [{TagName, Attributes, Children} | Rest]) ->
        {TagName, Attributes, Children, Rest};
    find_tag(TagName, [{_OtherTag, _Attributes, Children} | Rest]) ->
        find_tag(TagName, Children ++ Rest);
    find_tag(TagName, [_Other | Rest]) ->
        find_tag(TagName, Rest).
    
    find_tag_with_attr(_TagName, {_AttrKey, _AttrVal}, []) ->
        not_found;
    find_tag_with_attr(TagName, Attr = {AttrKey, AttrVal}, Tags) ->
        case find_tag(TagName, Tags) of
            not_found ->
                not_found;
            {TagName, Attributes, Children, Rest} ->
                case proplists:get_value(AttrKey, Attributes) of
                    AttrVal ->
                        Attributes;
                    _ -> 
                        find_tag_with_attr(TagName, Attr, Children ++ Rest) 
                end
        end.

(the <PIPE>s above should be “|“s and the <SEMI>s should be “;“s. Not sure why the syntax highlighter is doing that to them…

Assuming the above code is put into a module called “openid”, you get the following:

    1> openid:get_openid_server("blog.paulbonser.com")
    [{server,<<"http://www.livejournal.com/openid/server.bml">>},
     {delegate,<<"http://misterpib.livejournal.com/">>}]
    2>

As I said, this is the first step. Hopefully I’ll have some time very soon to get on with the next couple of steps, and then I’ll be done.

blog comments powered by Disqus